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.
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
  • each outgoing chunk contains at most 16,372 body bytes; its size prefix, body and trailing CRLF are sent together
  • empty payload buffers emit nothing; larger inputs are split without waiting for additional data
  • 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.

HttpClient and HttpServer are always full duplex in single mode. Waterwall Finish is a full teardown with no half-close, so an HTTP request or response body ending never closes the tunnel line on its own. http1-mode: "split" is the only shape where a body end propagates: the upload half is a separate connection, and its end is the in-band signal that the peer stopped sending.

If the remote HTTP peer keeps the transport open after completing the response, for example with HTTP keep-alive, the Waterwall line also remains open until the peer closes it or the downstream transport's configured idle timeout expires. The matching Waterwall HttpServer normally closes after its single tunneled response.

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

For each main line, HttpClient generates a fresh 128-bit CSPRNG pairing identifier from two fastRand64() values (16 bytes from the thread's ChaCha20 stream) and encodes it as exactly 32 lowercase hexadecimal characters. The value is ordinary routing metadata used to match the upload and download halves. Add TlsClient, EncryptionClient, or another security node if the deployment needs protected transport.

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:

  • the ordinary SETTINGS profile follows the pinned Chrome 154 desktop reference: header table 65,536 bytes, push disabled, initial stream receive window 6 MiB and header-list limit 262,144 bytes
  • the default maximum receive-frame size is 16 KiB, and the connection receive window is increased to 15 MiB
  • direct HTTP/2 sends the preface, initial SETTINGS and connection WINDOW_UPDATE together, followed by separate request HEADERS
  • outgoing DATA payloads are capped at 16,375 bytes, leaving nine bytes for the frame header within one 16 KiB TLS plaintext record; peer flow control may reduce them further
  • WebSocket mode additionally advertises ENABLE_CONNECT_PROTOCOL = 1; h2c uses the same SETTINGS profile
  • received DATA chunks are forwarded as Waterwall payload
  • upstream Finish sends an HTTP/2 END_STREAM

There is no delayed batching. These choices reduce specific record-boundary differences when TlsClient follows directly; they do not reproduce browser headers, request scheduling or connection pooling. Explicit TLS shaping or other intervening transforms can change the final records.

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.

Once the opening request and retained bytes in both directions have been sent, HttpClient destroys its HTTP state and permanently bypasses itself on that line. Reentrant input remains in FIFO order until this transition completes. HTTP body framing, HTTP/2, WebSocket and split connections remain active because they still have protocol or line-mapping work.

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.

On ordinary same-line connections, repeated Pause/Resume notifications pass through transparently and do not gate ready output. Split connections retain their mapping between the main line and the upload/download transports. HTTP framing, FIFO ordering and retained-input limits still apply.

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.

Node Metadata​

Source-backed metadata:

PropertyValue
node flagskNodeFlagBlocksSplice
can_have_prevtrue
can_have_nexttrue
layer_groupkNodeLayer4
layer_group_prev_nodekNodeLayer4
layer_group_next_nodekNodeLayer4
required_padding_left16 bytes