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 protocol | HttpClient settings | TlsClient 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.
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.
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
| Field | Type | Description |
|---|---|---|
host | string | Required. 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
| Field | Default | Description |
|---|---|---|
path | / | Request path. |
scheme | https | HTTP metadata scheme. This does not enable TLS by itself. |
port | 443 for https, otherwise 80 | HTTP metadata port. |
method | POST | Request method for non-WebSocket HTTP transport. |
user-agent | WaterWall/1.x | HTTP User-Agent value. |
http-version | HTTP/2 | Protocol mode. Accepts numeric and string aliases. |
upgrade | true only when http-version is both | Starts with an HTTP/1.1 upgrade request when allowed. |
upgrade-protocol | h2c | HTTP/1.1 upgrade token. Non-h2c tokens switch to raw byte forwarding after 101. |
upgrade-request-headers | none | Extra headers sent only on the upgrade request. |
upgrade-response-headers | none | Required headers that must be present in the 101 upgrade response. |
headers | none | Extra request headers. Values must be strings. |
content-type | none | Emits a built-in Content-Type value when matched by Waterwall's internal table. |
websocket | false | Uses HTTP for the opening handshake, then WebSocket frames for payload. |
websocket-origin | none | Optional Origin header for WebSocket. |
websocket-subprotocol | none | Optional requested WebSocket subprotocol. |
websocket-extensions | none | Optional extension header to send. Negotiated extensions are not implemented and are rejected. |
full-duplex | false | Kept for symmetry with HttpServer; client-side HTTP/1.1 already streams chunked request and response concurrently. |
http1-mode | single | HTTP/1.1 shape: single or split. The old boolean http1-split is also accepted. |
split | defaults described below | Settings for HTTP/1.1 split mode. |
verbose | false | Enables 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:
| Value | Meaning |
|---|---|
1, 1.1, http1, http1.1 | Force HTTP/1.1. |
2, 2.0, http2, h2 | Force direct HTTP/2. |
both, any, auto, 1.1+2 | Start 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
Finishsends the final0chunk 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:
| Field | Default | Description |
|---|---|---|
upload-method | top-level method | Method for the upload request. |
download-method | GET | Method for the download request. |
upload-path | top-level path | Path for the upload request. |
download-path | top-level path | Path for the download request. |
upload-headers | none | Extra headers for the upload half. |
download-headers | none | Extra headers for the download half. |
id-placement | query | Where the shared id is placed. |
id-name | wwid | Shared id field name. |
direction-placement | query | Where the direction marker is placed. |
direction-name | wwdir | Direction marker field name. |
upload-value | upload | Direction value for the upload half. |
download-value | download | Direction value for the download half. |
cache-bypass | true | Adds changing metadata to reduce accidental caching. |
cache-bypass-name | wwcb | Cache bypass field name. |
token | none | Optional shared token. |
token-placement | header | Where the optional token is placed. |
token-name | X-Waterwall-Token | Token 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.
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_STREAMSis set to1- 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
Finishsends an HTTP/2END_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-SettingsUpgrade: h2cHTTP2-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.
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
GETand expects101 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.
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": 1with"alpns": ["http/1.1"] - HTTP/2 WebSocket extended
CONNECT:"http-version": 2with"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 addingTlsClient. 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
HttpClientmust 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-h2cupgrade mode. - Using
http1-mode: "split"with HTTP/2 or WebSocket. Split mode is HTTP/1.1 only. - Expecting
HttpClientto expose HTTP headers to the previous node. It forwards body payload only. - Expecting this node to multiplex many HTTP/2 streams. Use
MuxClientbefore HTTP if you need Waterwall-level multiplexing. - Sending custom WebSocket extensions and expecting the tunnel to implement them.