HttpServer
HttpServer is the server-side counterpart to HttpClient. It parses an incoming HTTP request stream, removes HTTP framing, and forwards only request-body payload to the next Waterwall node. In the reverse direction, it wraps downstream payload as an HTTP response body.
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.
HttpServer does not terminate TLS. If traffic arrives as HTTPS, place TlsServer before HttpServer.
Typical Placement
TcpListener -> HttpServer -> SomeServiceTunnel
TcpListener -> TlsServer -> HttpServer -> SomeServiceTunnel
With a full client/server HTTP transport:
Client side:
TcpListener -> MuxClient -> HttpClient -> TlsClient -> TcpConnector
Server side:
TcpListener -> TlsServer -> HttpServer -> MuxServer -> TcpConnector
Basic Example
{
"name": "http-server",
"type": "HttpServer",
"settings": {
"http-version": "both",
"upgrade": true,
"host": "example.com",
"path": "/api/tunnel",
"method": "POST",
"status": 200,
"headers": {
"server": "waterwall"
}
},
"next": "service"
}
Required Settings
There are no tunnel-specific mandatory settings. The top-level settings object must exist and must not be empty.
Important defaults:
| Field | Default |
|---|---|
http-version | both |
upgrade | true when http-version is both |
path | / |
method | POST |
status | 200 |
websocket | false |
http1-mode | single |
Optional Settings
| Field | Default | Description |
|---|---|---|
http-version | both | Protocol mode. Accepts numeric and string aliases. |
upgrade | true only when http-version is both | Accepts HTTP/1.1 upgrade requests when allowed. |
upgrade-protocol | h2c | HTTP/1.1 upgrade token. Non-h2c tokens switch to raw byte forwarding after 101. |
upgrade-request-headers | none | Required headers that must be present in the upgrade request. |
upgrade-response-headers | none | Extra headers sent only on the 101 Switching Protocols response. |
host | none | Optional expected host. Mismatches are rejected. |
path | / | Expected request path. |
method | POST | Expected request method for non-WebSocket HTTP transport. |
status | 200 | Response status code. Current valid range is 100 through 599. |
headers | none | Extra response headers. Values must be strings. |
content-type | none | Emits a built-in Content-Type value when matched by Waterwall's internal table. |
websocket | false | Accepts WebSocket opening handshake, then uses WebSocket frames for payload. |
websocket-origin | none | Optional required WebSocket Origin. |
websocket-subprotocol | none | Optional required subprotocol. It is echoed in the response. |
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 pairing. |
no-split-upload-buffering-limit | false | Test-oriented option that disables the normal upload buffering limit before the download half joins. |
verbose | false | Enables more protocol, handshake, and framing logs. |
content-type is selected from Waterwall's built-in content-type table. For a custom response value that is not in the table, set it through headers.
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 | Detect direct HTTP/2 or handle HTTP/1.1, including optional upgrade. |
HTTP/1.1 Single Mode
In http1-mode: "single", one HTTP/1.1 request carries the upload side and one HTTP/1.1 response carries the download side:
- request headers are consumed by
HttpServer - request body bytes are forwarded to the next node as Waterwall payload
- response payload from the next node is emitted as the HTTP response body
- the response uses
Transfer-Encoding: chunked - downstream
Finishsends the final response chunk before the real Waterwall finish is forwarded
The request parser supports chunked bodies, Content-Length, and requests with no body. Chunked trailers are consumed 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.
HTTP/1.1 Split Mode
Split mode pairs two HTTP/1.1 requests into one Waterwall line:
- the upload request body is decoded and sent to the next node
- the download request receives the response body generated from downstream Waterwall payload
- both halves are matched by shared id, direction, and optional token metadata
HttpClient supplies a fresh 128-bit CSPRNG pairing ID made from two fastRand64() values (16 bytes from the thread's ChaCha20 stream) and formatted as 32 lowercase hexadecimal characters. The server uses it as ordinary routing metadata to match the upload and download halves. Add TlsServer, EncryptionServer, or another security node if the deployment needs protected transport.
The separate upload body's end propagates upstream Finish on the logical main line.
This mode is designed to match HttpClient split mode on the other side.
{
"name": "http-server",
"type": "HttpServer",
"settings": {
"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",
"token": "shared-token",
"token-placement": "header",
"token-name": "X-Tunnel-Token"
},
"headers": {
"Cache-Control": "no-store"
}
},
"next": "service"
}
Common split fields:
| Field | Default | Description |
|---|---|---|
upload-method | top-level method | Method expected for the upload request. |
download-method | GET | Method expected for the download request. |
upload-path | top-level path | Path expected for the upload request. |
download-path | top-level path | Path expected for the download request. |
id-placement | query | Where the shared id is read from. |
id-name | wwid | Shared id field name. |
direction-placement | query | Where the direction marker is read from. |
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. |
token | none | Optional shared token required from both halves. |
token-placement | header | Where the optional token is read from. |
token-name | X-Waterwall-Token | Token field name. |
Placement values can be query, header, cookie, or path. Path templates can contain {id}, {direction}, {cache}, and {token}.
Split mode requires http-version = 1 and cannot be combined with websocket.
The split waiting limit is 131070 * max(1, ceil(L / 32768)) bytes, where L
is the line pool waiting-budget basis (64 KiB in S1/S2, 1 MiB in higher profiles),
independent of ordinary buffer sizes and the splice limit. Close occurs strictly above
this limit: 262,140 bytes in S1/S2 and 4,194,240 bytes in higher profiles. Header parsing
and available pairing run first; coalesced body bytes do not count toward the
header-size limit. Paired bodies retain normal HTTP framing validation.
no-split-upload-buffering-limit disables the normal protection that closes a split upload side if it sends too much data before the matching download side has joined. It is mainly useful for controlled tests and unusual multi-worker timing cases, not normal deployments.
HTTP/2 Mode
In direct HTTP/2 mode, HttpServer accepts one logical HTTP/2 request stream for the Waterwall line:
- request DATA frames become upstream Waterwall payload
- downstream Waterwall payload becomes response DATA frames
- request
END_STREAMis recorded internally and the line remains open until transport close - downstream
Finishsends responseEND_STREAM
Important implementation details:
MAX_CONCURRENT_STREAMSis set to1- initial stream window is set to 1 MiB
- maximum frame size is set to 32 KiB
- extra HTTP/2 streams are refused with
REFUSED_STREAM - WebSocket mode enables
SETTINGS_ENABLE_CONNECT_PROTOCOL
This node is intentionally a single-stream HTTP transport, not a general HTTP/2 stream multiplexer.
Upgrade Behavior
When http-version is both and upgrade is enabled, HttpServer can accept HTTP/1.1 upgrade requests.
For default h2c, the request must include the expected upgrade metadata:
ConnectioncontainingUpgradeandHTTP2-SettingsUpgrade: h2cHTTP2-Settings
After accepting the upgrade, HttpServer sends 101 Switching Protocols, applies the peer settings, and switches into HTTP/2 mode. The original upgraded request is stream 1; the current design waits for one post-upgrade HTTP/2 tunnel stream for payload.
If the upgrade request carries a body, the server treats that case conservatively instead of trying to combine the body with the upgraded HTTP/2 session.
If upgrade-protocol is set to a custom token other than h2c, a matching request receives 101 Switching Protocols and the tunnel becomes raw bidirectional byte forwarding after the handshake. upgrade-request-headers can require extra request headers, and upgrade-response-headers can add response headers for that 101.
After the complete 101 response and admitted raw tails have been delivered,
HttpServer destroys its HTTP state and permanently bypasses itself on that line.
Reentrant raw payload during this handoff stays in FIFO order, with a limit of
2 MiB of allocation charge and 1,024 buffers per directional queue; overflow
closes the line. HTTP body, HTTP/2, WebSocket and split modes remain active for
framing or line mapping. Same-line Pause/Resume notifications are transparent and
repeatable, including during upgrade, and do not postpone ready output.
WebSocket Mode
With websocket: true, HttpServer accepts a WebSocket handshake and then transports Waterwall payload as WebSocket frames.
HTTP/1.1 WebSocket requirements:
- method must be
GET - request must contain WebSocket upgrade headers
Sec-WebSocket-Keymust be validSec-WebSocket-Versionmust be13- request body is not accepted
- optional
host,path,websocket-origin, andwebsocket-subprotocolchecks must pass
HTTP/2 WebSocket requirements:
- request must use extended
CONNECT :protocolmust bewebsocket:pathand authority/host must match configured expectations when setSec-WebSocket-Versionmust be13
After the handshake:
- client-to-server frames must be masked
- server-to-client frames are sent unmasked
- downstream Waterwall payload is sent as WebSocket binary frames
- text, binary, and continuation frames from the peer are forwarded as plain payload
- ping and pong are handled internally
- a close frame is sent before the real Waterwall finish
Finish And Lifecycle Behavior
HttpServer sends protocol-final bytes before forwarding some Waterwall Finish events:
- HTTP/1.1 sends the final response chunk
- HTTP/2 sends response
END_STREAM - WebSocket sends a close frame
The implementation marks the relevant direction as finished before sending final bytes, then destroys local HTTP state before forwarding the real Waterwall Finish. This keeps final-byte backpressure from reflecting pause/resume callbacks toward a side that has already finished.
Split mode creates and manages the logical Waterwall line that joins the upload and download HTTP halves. The transport halves are closed without treating them as independent service connections.
Buffer Padding
HttpServer advertises 16 bytes of left padding for its framing paths. Keep this in mind when composing tunnels that also prepend protocol bytes.
Common Mistakes
- Placing
HttpServerdirectly afterTcpListenerwhen the incoming traffic is TLS encrypted. UseTlsServerfirst. - Expecting headers to pass through to the next node. The next node receives only body payload.
- Combining HTTP/1.1 split mode with WebSocket or HTTP/2.
- Expecting multiple independent HTTP/2 streams to become multiple Waterwall lines.
- Using custom WebSocket extensions. The WebSocket path transports unextended frames.
Node Metadata
Source-backed metadata:
| Property | Value |
|---|---|
| node flags | kNodeFlagBlocksSplice |
can_have_prev | true |
can_have_next | true |
layer_group | kNodeLayer4 |
layer_group_prev_node | kNodeLayer4 |
layer_group_next_node | kNodeLayer4 |
required_padding_left | 16 bytes |