icey's HTTP layer is fast because it is not trying to hide what the connection is doing.
There is a parser, a connection, an adapter, a request, a response, and a few very specific state transitions. Once you understand those, the rest of the module reads cleanly.
This page is about that flow.
The first thing to get straight is this:
start() starts an outgoing HTTP transactionsend() writes bytes on a live connectionThat split is intentional.
If you find yourself trying to use send() to start an HTTP client request, you are using the wrong API.
At the center of the module is http::Connection.
It owns:
TCPSocket or SSLSocketConnectionAdapterRequestResponseThe parser lives inside the adapter, not inside the connection itself.
The adapter sits between the socket and the connection. On a plain HTTP connection it owns the llhttp parser and turns transport events into:
On upgrade, the adapter can be replaced. That is how WebSocket takes over without rebuilding the connection object from scratch.
The server side has one important idea now: the connection state is explicit.
ServerConnection moves through these states:
| State | Meaning |
|---|---|
ReceivingHeaders | request line and headers are being parsed |
ReceivingBody | request body is still arriving |
DispatchingOrSending | request is complete and the responder or handler is producing a response |
Streaming | response is intentionally long-lived |
Upgraded | HTTP is over; another protocol now owns the transport |
Closing | shutdown has started |
Closed | done |
That state machine exists because the older boolean-inference model stops holding once you have keep-alive reuse, streaming, and protocol upgrade in the same library.
The normal server request looks like this:
TCP accept
-> parser reads request line and headers
-> onHeaders()
-> optional body chunks via onPayload()
-> onComplete()
-> handler or responder writes response
-> keep alive or closeIn code, srv.Connection fires once the request is ready to handle.
That is why the basic server example looks simple: by the time your handler runs, the connection is already past header parsing and is in the dispatch/send phase.
Bodies do not change the model much, but they do change timing.
If the request has a body:
PayloadThe important rule is the same one from net:
MutableBuffer for payload is borrowed for that callback onlyIf you need to keep it, copy it or move it across a retained boundary immediately.
A reusable HTTP server connection does not die after one request. It gets reset and goes back to ReceivingHeaders.
That reset matters for correctness:
This is one of those areas where "almost reset" produces very ugly bugs. The current server code is strict about returning pooled connections to a real clean parse state.
icey treats long-lived streaming responses as a different kind of connection state.
That matters because they should not be reaped like ordinary idle keep-alive sockets.
Use streaming mode when the response is intentionally open:
Once a server connection enters Streaming, the idle reaper backs off. When the stream ends, the connection transitions back into normal HTTP lifecycle rules or closes.
That is very different from simply forgetting to close a one-shot response.
WebSocket is the cleanest example of why the adapter model exists.
The flow is:
HTTP request with Upgrade headers
-> HTTP connection validates the upgrade
-> replaceAdapter(ws::ConnectionAdapter)
-> send 101 Switching Protocols
-> mark connection as Upgraded
-> frame-based I/O takes overAfter that point, the connection is not "an HTTP connection that also does WebSocket." It is upgraded transport running through the WebSocket adapter on the same underlying socket.
That distinction matters for:
It is also why upgrade tail bytes had to be handled carefully. If HTTP parsing stops at the handshake boundary but there are already WebSocket bytes in the same TCP read, those bytes still need to make it into the new adapter.
Client-side HTTP is simpler once you keep the verbs straight.
The flow is:
build URL and request
-> start()
-> connect socket if needed
-> write request line, headers, optional body
-> receive response headers
-> receive payload chunks
-> complete or closeClientConnection::start() is the transaction entry point.
Then:
Headers fires when response headers are readyPayload fires for each body chunkComplete fires once the whole response is receivedClose fires when the connection closesFor WebSocket client connections, Connect means the WebSocket handshake is complete, not just that the TCP socket connected.
That is the correct contract. Anything else would make the API ambiguous.
sendHeader() and Header Auto-SendThe server and the connection adapter can send headers for you, but this is still explicit enough to reason about.
sendHeader() writes the current outgoing headerheaderAutoSendEnabled controls that behavior for the next outgoing pathYou do not need to micromanage it in the common cases, but you also do not have to guess what the connection is doing.
That applies to:
send() is the fast pathUse it when the payload naturally lives long enough.
sendOwned() is for assembled temporary outputUse it for:
This is the same performance contract as the lower net layer, just applied to HTTP and upgrade paths.
The HTTP module does not try to pretend everything is one high-level request abstraction.
It keeps three things explicit:
That is why the server can handle:
without turning into a giant pile of special cases.