Skip to main content

HttpClient

HttpClient is a stream tunnel that wraps Waterwall payload inside an HTTP client request. On the forward path it creates HTTP request framing, and on the return path it removes HTTP response framing so the previous node receives only response-body bytes.

It supports HTTP/1.1, direct HTTP/2, HTTP/1.1 to h2c upgrade, custom HTTP/1.1 upgrade tokens, HTTP/1.1 split mode, and WebSocket transport mode.

HttpClient only formats HTTP. It does not create TLS. If the traffic must be HTTPS on the wire, place TlsClient after HttpClient.

Typical Placement

SomeTunnel -> HttpClient -> TcpConnector
SomeTunnel -> HttpClient -> TlsClient -> TcpConnector

With TLS:

TcpListener -> MuxClient -> HttpClient -> TlsClient -> TcpConnector

Without TLS:

TcpListener -> MuxClient -> HttpClient -> TcpConnector

TLS ALPN Compatibility

When TlsClient follows HttpClient, the configured HTTP version and the protocols offered through TLS ALPN must agree. HttpClient chooses its wire protocol from http-version. It does not receive the server's negotiated ALPN from TlsClient, and it cannot change HTTP modes after the TLS handshake. Keeping these settings consistent is the user's responsibility.

Use these pairings when you need a deterministic one-protocol offer:

Intended protocolHttpClient settingsTlsClient setting
HTTP/1.1, HTTP/1.1 split mode, or classic WebSocket Upgrade"http-version": 1"alpns": ["http/1.1"]
Direct HTTP/2 or WebSocket over HTTP/2 extended CONNECT"http-version": 2"alpns": ["h2"]

For WebSocket over HTTP/2, set "http-version": 2 together with "websocket": true. HttpClient then uses the extended CONNECT path. Choose HTTP/1.1 instead only when the peer expects the classic GET/101 Switching Protocols upgrade.

caution

Offering both h2 and http/1.1 in TlsClient is valid, commonly used, and closer to Chrome's default ClientHello. When using that offer, the user must know which protocol the target server will select and configure this HttpClient for that version. For example, an ordinary Cloudflare-proxied HTTPS hostname with HTTP/2 enabled selects h2 when it is offered, so HttpClient should use HTTP/2. If the target's choice is unknown or can vary, offer only the protocol that matches HttpClient.

http-version: "both" and its aliases are not ALPN-aware. They implement an HTTP/1.1-to-h2c application-layer upgrade and do not follow the TLS ALPN result. Select an explicit HTTP version when TlsClient is next in the chain.

caution

As of July 2026, the h2 assumption above applies to ordinary HTTPS through a Cloudflare-proxied hostname with HTTP/2 enabled, not WebSocket. Cloudflare's currently documented proxied WebSocket behavior does not support HTTP/2 WebSocket extended CONNECT (RFC 8441).

For Cloudflare WebSocket, configure HttpClient for HTTP/1.1 WebSocket Upgrade and configure the following TlsClient with only "alpns": ["http/1.1"]. Offering both makes Cloudflare select h2: an HTTP/1.1 HttpClient would then mismatch the negotiated protocol, while an HTTP/2 HttpClient would enter the unsupported extended CONNECT path. Re-check Cloudflare's current capabilities before depending on either behavior, because provider settings and support can change.

Basic Example

{
"name": "http-client",
"type": "HttpClient",
"settings": {
"host": "example.com",
"path": "/api/tunnel",
"scheme": "https",
"port": 443,
"method": "POST",
"http-version": 2,
"headers": {
"x-client-name": "waterwall"
}
},
"next": "tls-or-transport"
}

Required Settings

FieldTypeDescription
hoststringRequired. Used as the HTTP/1.1 Host header and HTTP/2 :authority pseudo-header.

The top-level settings object must exist and must not be empty.

Optional Settings

FieldDefaultDescription
path/Request path.
schemehttpsHTTP metadata scheme. This does not enable TLS by itself.
port443 for https, otherwise 80HTTP metadata port.
methodPOSTRequest method for non-WebSocket HTTP transport.
user-agentWaterWall/1.xHTTP User-Agent value.
http-versionHTTP/2Protocol mode. Accepts numeric and string aliases.
upgradetrue only when http-version is bothStarts with an HTTP/1.1 upgrade request when allowed.
upgrade-protocolh2cHTTP/1.1 upgrade token. Non-h2c tokens switch to raw byte forwarding after 101.
upgrade-request-headersnoneExtra headers sent only on the upgrade request.
upgrade-response-headersnoneRequired headers that must be present in the 101 upgrade response.
headersnoneExtra request headers. Values must be strings.
content-typenoneEmits a built-in Content-Type value when matched by Waterwall's internal table.
websocketfalseUses HTTP for the opening handshake, then WebSocket frames for payload.
websocket-originnoneOptional Origin header for WebSocket.
websocket-subprotocolnoneOptional requested WebSocket subprotocol.
websocket-extensionsnoneOptional extension header to send. Negotiated extensions are not implemented and are rejected.
full-duplexfalseKept for symmetry with HttpServer; client-side HTTP/1.1 already streams chunked request and response concurrently.
http1-modesingleHTTP/1.1 shape: single or split. The old boolean http1-split is also accepted.
splitdefaults described belowSettings for HTTP/1.1 split mode.
verbosefalseEnables more protocol, handshake, and framing logs.

Supported HTTP method names include common methods such as GET, POST, PUT, PATCH, DELETE, HEAD, CONNECT, OPTIONS, and the extended WebDAV-style methods supported by Waterwall's HTTP method table.

content-type is selected from Waterwall's built-in content-type table. Common examples include application/json, text/plain, application/octet-stream, and multipart/form-data. For a custom value that is not in the table, set it through headers instead.

HTTP Version Modes

http-version accepts these values:

ValueMeaning
1, 1.1, http1, http1.1Force HTTP/1.1.
2, 2.0, http2, h2Force direct HTTP/2.
both, any, auto, 1.1+2Start in HTTP/1.1 and optionally upgrade to HTTP/2 cleartext with h2c.

Current default: direct HTTP/2.

HTTP/1.1 Single Mode

In http1-mode: "single", one HTTP/1.1 connection carries both directions:

  • upstream Waterwall payload becomes the request body
  • downstream response body becomes Waterwall payload
  • request payload is sent with Transfer-Encoding: chunked
  • upstream Finish sends the final 0 chunk before the Waterwall finish is forwarded

The response parser supports chunked responses, Content-Length, until-close response bodies, and response codes that must not have a body. Informational 1xx responses are skipped internally.

HTTP/1.1 Split Mode

Split mode uses two HTTP/1.1 requests for one Waterwall line:

  • an upload request carries Waterwall payload to the server
  • a download request receives the response body from the server
  • both halves are paired by shared metadata such as id, direction, and optional token

This is useful when a proxy, CDN, or middlebox does not behave well with a long bidirectional HTTP/1.1 request and response on the same connection.

{
"name": "http-client",
"type": "HttpClient",
"settings": {
"host": "cdn.example",
"http-version": 1,
"http1-mode": "split",
"path": "/tunnel",
"split": {
"upload-method": "POST",
"download-method": "GET",
"id-placement": "query",
"id-name": "sid",
"direction-placement": "query",
"direction-name": "part",
"upload-value": "up",
"download-value": "down",
"token": "shared-token",
"token-placement": "header",
"token-name": "X-Tunnel-Token",
"upload-headers": {
"Cache-Control": "no-store"
},
"download-headers": {
"Accept": "application/octet-stream"
}
}
},
"next": "transport"
}

Common split fields:

FieldDefaultDescription
upload-methodtop-level methodMethod for the upload request.
download-methodGETMethod for the download request.
upload-pathtop-level pathPath for the upload request.
download-pathtop-level pathPath for the download request.
upload-headersnoneExtra headers for the upload half.
download-headersnoneExtra headers for the download half.
id-placementqueryWhere the shared id is placed.
id-namewwidShared id field name.
direction-placementqueryWhere the direction marker is placed.
direction-namewwdirDirection marker field name.
upload-valueuploadDirection value for the upload half.
download-valuedownloadDirection value for the download half.
cache-bypasstrueAdds changing metadata to reduce accidental caching.
cache-bypass-namewwcbCache bypass field name.
tokennoneOptional shared token.
token-placementheaderWhere the optional token is placed.
token-nameX-Waterwall-TokenToken field name.

Placement values can be query, header, cookie, or path. The parser also accepts aliases such as query-param, http-header, and path-template.

Path templates can contain {id}, {direction}, {cache}, and {token}.

Split configuration can use either direct fields such as upload-method and download-method, or nested upload and download objects with per-half method, path, and headers.

warning

Split mode requires http-version = 1 and cannot be combined with websocket.

HTTP/2 Mode

In HTTP/2 mode, HttpClient opens one logical HTTP/2 request stream per Waterwall line. The current design is intentionally single-stream for each line; it is not a general multi-stream HTTP/2 multiplexer.

Important implementation details:

  • MAX_CONCURRENT_STREAMS is set to 1
  • initial stream window is set to 1 MiB
  • maximum frame size is set to 32 KiB
  • received DATA chunks are forwarded as Waterwall payload
  • upstream Finish sends an HTTP/2 END_STREAM

If you need many independent logical connections over one transport, use a Waterwall node such as MuxClient before HttpClient instead of expecting HTTP/2 stream multiplexing here.

Upgrade Behavior

When http-version is both and upgrade is enabled, HttpClient starts with an HTTP/1.1 upgrade request.

For the default h2c upgrade, the request includes:

  • Connection: Upgrade, HTTP2-Settings
  • Upgrade: h2c
  • HTTP2-Settings

If the server accepts the upgrade with 101 Switching Protocols, Waterwall switches the line into HTTP/2 mode. The original upgraded stream is cancelled and a fresh single HTTP/2 tunnel stream is opened for payload.

If the server returns a normal non-101 HTTP response, the tunnel stays in HTTP/1.1 mode and sends the buffered request payload as chunked HTTP/1.1 body data.

If upgrade-protocol is set to a custom token other than h2c, a successful 101 switches the tunnel to raw bidirectional byte forwarding after the upgrade handshake. This is useful when an HTTP-looking opening handshake is needed before another stream protocol takes over.

caution

HTTP/1.1 upgrade with a request body is not a safe shape for the current single-stream design. Do not rely on upgrade-with-body behavior.

WebSocket Mode

With websocket: true, HTTP is used only for the opening handshake:

  • HTTP/1.1 WebSocket uses GET and expects 101 Switching Protocols
  • HTTP/2 WebSocket uses extended CONNECT
  • upstream payload buffers are sent as WebSocket binary frames
  • text, binary, and continuation frames received from the peer are forwarded as plain payload
  • ping and pong control frames are handled internally
  • a close frame is sent before the real Waterwall finish

HTTP/1.1 WebSocket responses are validated, including Sec-WebSocket-Accept. Server-to-client frames must not be masked. If the peer negotiates WebSocket extensions, the connection is rejected because extensions are not implemented by this tunnel.

For HTTP/2 WebSocket, the peer must advertise SETTINGS_ENABLE_CONNECT_PROTOCOL = 1 before the extended CONNECT request is sent.

note

http-version: "both" with websocket: true keeps the WebSocket opening handshake on HTTP/1.1.

When TlsClient follows this node, match the WebSocket path to the ALPN the target server will select:

  • HTTP/1.1 WebSocket Upgrade: "http-version": 1 with "alpns": ["http/1.1"]
  • HTTP/2 WebSocket extended CONNECT: "http-version": 2 with "alpns": ["h2"]

A Chrome-like offer containing both values is also valid when the target's selection is known and stable, but HttpClient must be configured for that known result because it cannot observe the server's selection. Cloudflare WebSocket is the exception described above and must be forced to HTTP/1.1.

Finish And Lifecycle Behavior

HttpClient has to send protocol-final bytes before forwarding some Waterwall Finish events:

  • HTTP/1.1 sends the final chunk
  • HTTP/2 sends END_STREAM
  • WebSocket sends a close frame

The implementation marks the relevant direction as finished before sending final protocol bytes, then destroys local HTTP state before forwarding the real Waterwall Finish. This prevents pause/resume or close callbacks caused by final-byte flushing from being reflected back into a side that already finished.

Buffer Padding

HttpClient advertises 16 bytes of left padding. That padding is used by the node's framing paths and must be preserved by surrounding tunnel composition.

Common Mistakes

  • Setting scheme: "https" without adding TlsClient. The scheme is HTTP metadata only.
  • Offering multiple ALPN protocols without knowing which one the target will select. A Chrome-like dual offer is valid, but HttpClient must be configured for the known result because the ALPN selection is not passed back to this node.
  • Using http-version: "both" as if it were TLS protocol negotiation. It is an HTTP/1.1-to-h2c upgrade mode.
  • Using http1-mode: "split" with HTTP/2 or WebSocket. Split mode is HTTP/1.1 only.
  • Expecting HttpClient to expose HTTP headers to the previous node. It forwards body payload only.
  • Expecting this node to multiplex many HTTP/2 streams. Use MuxClient before HTTP if you need Waterwall-level multiplexing.
  • Sending custom WebSocket extensions and expecting the tunnel to implement them.