Skip to content

Rust HTTP backend: kj's HTTP interfaces on hyper, rustls and tungstenite - #7438

Draft
danlapid wants to merge 1 commit into
mainfrom
dlapid/rustHttpLean
Draft

danlapid wants to merge 1 commit into
mainfrom
dlapid/rustHttpLean

Conversation

@danlapid

Copy link
Copy Markdown
Collaborator

Under --//:io_backend=rust, workerd's HTTP/1.1 client and server, WebSockets, CONNECT tunnels and TLS run in Rust on hyper, hyper-util, rustls and tungstenite's frame codec. workerd is still written against kj's HTTP interfaces (kj::HttpClient, HttpService, HttpServer, WebSocket, SecureNetworkWrapper); only their implementations change, by symbol override, as the I/O layer does. Product code is unchanged, and the default C++ build is untouched.

Layout:

  • kj-hyper (src/rust/cxx/kj-hyper, ~4.3k lines): hyper's pooling and single-stream clients, hyper's server fused into one kj event per connection, the WebSocket frame loop with permessage-deflate (flate2/zlib-rs), rustls TLS, and the kj <-> tokio stream bridges, with a thin C++ seam.
  • kj-http-tokio.c++: the kj:: HTTP entry points over kj-hyper.
  • tls-network: config::TlsOptions as a kj::SecureNetworkWrapper for both engines, so server.c++ has no per-backend #if.
  • kj-rs-http: Rust bindings for kj's header table and HttpHeaders.
  • capnproto (companion change): kj-http-types holds everything that is not kj's wire implementation -- header codec, permessage-deflate negotiation, WebSocket pump and pipe, client/service adapters -- so this backend reuses kj's own code for those and links without kj-http-impl.

Behavior follows kj's contracts where workerd relies on them:

  • Wire fidelity: header spellings (hyper HeaderCaseMap patch), reason phrases, kj's Content-Length and Connection: close rules, drain finishing a partially received request.
  • Errors: HttpServerErrorHandler/WebSocketErrorHandler run as in kj's server; kj exceptions survive the trip through hyper.
  • WebSocket: text passes through unvalidated, the application owns Close, maxSize is checked from the frame header, pongs queue behind a send in flight, wire byte counters. The server refuses bad handshakes with 400, or 426 with Sec-WebSocket-Version for an unsupported version (RFC 6455 4.2.2); the client validates Sec-WebSocket-Accept and takes its key from the configured EntropySource.
  • Disconnects: whenWriteDisconnected() and whenAborted() resolve on peer hangup, and a peer that disconnects cancels its connection's service calls.
  • Server timeouts: headerTimeout applies to each request head.
  • TLS: trustBrowserCas uses the platform verifier, otherwise only trustedCertificates are trusted (also directly, as OpenSSL); minVersion TLS 1.3 and cipherList's TLS 1.2 suites are honored.

Crate patches: http/httparse lenient header values, hyper's public header case map, graceful shutdown finishing a partial request, lazily read client response bodies.

Tests: kj-http-test (48) and tls-network-test (14) check kj's HTTP and TLS entry points against the wire and the TLS policy, and run unchanged on both backends. kj-hyper_test covers the deflate codec (RFC 7692 vectors) and the TLS option mapping. server-test gains a WebSocket compression case, and TestServer::wait() lets events due at its final instant settle. bazel test //src/... passes under the rust backend; the cxx build's server-test, kj-http-test and tls-network-test pass.

Requires the capnproto kj-http split (kj-http-types).

@github-actions

Copy link
Copy Markdown

The generated output of @cloudflare/workers-types has been changed by this PR. If this is intentional, run just generate-types to update the snapshot. Alternatively, you can download the full generated types:

Full Type Diff

@ask-bonk

ask-bonk Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

@danlapid Bonk workflow was cancelled.

View workflow run · To retry, trigger Bonk again.

@danlapid
danlapid force-pushed the dlapid/rustHttpLean branch 2 times, most recently from 5ab44a0 to ef5c6ce Compare September 20, 2026 19:38
Under --//:io_backend=rust, workerd's HTTP/1.1 client and server, WebSockets,
CONNECT tunnels and TLS run in Rust on hyper, rustls and tungstenite's frame
codec. workerd is still written against kj's HTTP interfaces (kj::HttpClient,
HttpService, HttpServer, WebSocket, SecureNetworkWrapper); only their
implementations change, by symbol override, as the I/O layer does. Product code
is unchanged, and the --//:io_backend=cxx build keeps kj's implementations.

Layout:
- kj-hyper (src/rust/cxx/kj-hyper): HTTP/1.1 clients over hyper's connection
  API (a connection pool dialed through kj networks, or one connection over a
  kj stream), hyper's server fused into one kj event per connection, the
  WebSocket frame loop with permessage-deflate (flate2/zlib-rs), rustls TLS,
  and the kj <-> tokio stream bridges, with a thin C++ seam. Upgrades are
  taken over from hyper by hand, so transports need not be Send; handles are
  single-threaded, as kj's HTTP objects are, and operations own their state.
  The lifetime, cancellation and threading rules are in src/rust/cxx/AGENTS.md.
- kj-http-tokio.c++: the kj:: HTTP entry points over kj-hyper.
- tls-network: config::TlsOptions as a kj::SecureNetworkWrapper, one source
  per engine (rustls or OpenSSL), so server.c++ has no per-backend #if.
- src/rust/kj: the kj crate links kj-http-types only.
- capnproto (companion change): kj-http-types holds everything that is not
  kj's wire implementation -- header codec, permessage-deflate negotiation,
  WebSocket pump and pipe, client/service adapters -- so this backend reuses
  kj's own code for those and links without kj-http-impl.

Behavior follows kj's contracts where workerd relies on them:
- Wire fidelity: header spellings both ways (hyper HeaderCaseMap patch),
  reason phrases, kj's Content-Length and Connection: close rules, drain
  finishing a partially received request and stopping the listener.
- Errors: HttpServerErrorHandler/WebSocketErrorHandler run as in kj's server,
  for CONNECT too; kj exceptions survive the trip through hyper. Unsupported
  HttpServerSettings are documented in hyper-http.h.
- Bodies: a write beyond the declared length fails ("overwrote
  Content-Length"); a response to HEAD keeps the application's Content-Length
  and discards its body.
- Clients: requests over one stream wait their turn for its connection; an
  abandoned response body fails later requests as kj's does; a connection is
  reused only once its response body has been read.
- WebSocket: text passes through unvalidated, the application owns Close,
  maxSize is checked from each frame header against what is left of the
  message, one operation writes at a time and writes what was owed meanwhile
  (a pong, a protocol-error Close, disconnect()'s shutdown), writes complete
  once flushed, abort() fails operations in flight, a send cancelled mid-frame
  ends sending, wire byte counters. The server refuses
  bad handshakes with 400, or 426 with Sec-WebSocket-Version (RFC 6455 4.2.2);
  the client validates Sec-WebSocket-Accept and takes its key from the
  configured EntropySource.
- Disconnects: whenWriteDisconnected() and whenAborted() resolve on peer
  hangup (and fail as observing it fails), and a peer that disconnects cancels
  its connection's service calls.
- Server timeouts: headerTimeout applies to each request head.
- TLS: handshakes, client authentication included, complete before a stream is
  handed out, and a listener runs them concurrently. trustBrowserCas uses the
  platform verifier, otherwise only trustedCertificates are trusted; a trusted
  certificate is also trusted directly, as OpenSSL does, with its validity
  and key usage still checked. minVersion TLS 1.3 is honored; cipherList must
  name TLS 1.2 suites, and requireClientCerts with trustBrowserCas is refused.
  A peer closing without close_notify ends reads with DISCONNECTED, as kj's TLS
  does.

Crate patches: http/httparse lenient header values, hyper's public header case
map, graceful shutdown finishing a partial request, lazily read client response
bodies.

Tests: kj-http-test (53) and tls-network-test (16) check kj's HTTP and TLS
entry points against the wire and the TLS policy, and run unchanged on both
backends. kj-hyper_test covers the deflate codec (RFC 7692 vectors), the TLS
options and direct trust, WebSocket writer sharing, abort and cancellation,
body length accounting, flushing writes, TLS waker sharing and the hangup
watch. Under the rust backend server-test drives
its loop with a TokioEventPort; it gains a WebSocket compression case, and
TestServer::wait() lets events due at its final instant settle.
rust-io-link-check also refuses kj's HTTP implementation and kj-tls in the
linked binary.

Known failure under the rust backend: tls-nodejs-test's testTlsOnEmptySocket.
Its sidecar ends the connection before the client's Finished arrives, and node
then sends no close_notify; the strict end-of-stream above reports that as
DISCONNECTED. The cxx backend writes Finished sooner and wins the race.

Requires the capnproto kj-http split (kj-http-types).

Co-Authored-By: Claude Code <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant