Skip to content

server: free serve_connection over any HTTP service; axum::serve / serve_tls on the shared loop and driver - #308

Merged
iainmcgin merged 2 commits into
mainfrom
rpb/c-serve-connection
Sep 18, 2026
Merged

iainmcgin merged 2 commits into
mainfrom
rpb/c-serve-connection

Conversation

@rpb-ant

@rpb-ant rpb-ant commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

The connection driver becomes a public free function over any tower HTTP service. connectrpc::axum::serve_tls, which had its own copy, and the new connectrpc::axum::serve now run on it and on Server's accept loop.

connectrpc::axum::serve(listener, app) // plaintext, new
    .with_connection_config(ConnectionConfig::new().with_max_connection_age(age))
    .with_graceful_shutdown(signal)
    .await?;
// a custom loop, any tower HTTP service:
let closed = server::serve_connection(stream, info, app.clone(), config.clone(), drain).await;
  • server::serve_connection(io, info, service, config, shutdown) binds nothing to a runtime until its future is first polled, so a loop chooses where a connection runs by where it spawns the future. A tokio socket stays on the I/O driver that registered it; the guide's "Custom accept loops" section moves it with into_std / TcpStream::from_std, which does not work once the stream is wrapped in TLS.
  • connectrpc::axum now needs axum + server; serve_tls still needs server-tls. ServeTls is renamed Serve, with a #[deprecated(since = "0.10.0")] alias, and Serve::with_connection_config is new.
  • On serve_tls, a handler that panics before responding now gets a 500 with a Connect internal body instead of a dropped connection, the standalone server's connection defaults and ConnectionConfig apply, and the Serve future owns the connection tasks.
  • On every path, a panic while a response body is produced is logged and resets the stream (INTERNAL_ERROR) or closes the HTTP/1.1 connection, instead of unwinding through the connection task. HTTP/2 extended CONNECT is advertised, and the accept loop pauses for up to a second after EMFILE / ENFILE.
  • Fixes a bug also in 0.9.0: with max_connection_idle set, a request stopped counting as in flight once its response head was returned, so a live streaming response could be closed as idle. It now counts until hyper drops the body, so a client that stops reading keeps its connection until max_connection_age retires it.

Part of #191 and #49.

rpb-ant and others added 2 commits September 17, 2026 22:48
…/ `serve_tls` run on the shared loop and driver

The per-connection driver is generalised from `ConnectRpcService<D>` to
any `tower::Service<Request<Incoming>, Response = Response<B>>` and
published as `server::serve_connection(io, info, service, config,
shutdown) -> ConnectionClosed`, with panic isolation applied per
connection (`CatchPanic` around the caller's service) and hyper's
`serve_connection_with_upgrades`. `Server::serve_connection` delegates to
it, and the crate-private accept loop takes the same service bounds.

`connectrpc::axum` then needs only `axum` + `server`: `serve(listener,
router)` (plaintext) and `serve_tls` return `Serve` (the renamed
`ServeTls`, kept as a deprecated alias), which holds a `ConnectionConfig`
(`with_connection_config`) and runs the router on the shared accept loop
and driver instead of its own copy. That fixes `serve_tls` silently
lacking max age / idle / max requests, HTTP/2 settings and panic
isolation, and makes its connection tasks owned by the future rather
than detached. CI lints and checks the `server` and `axum,server`
feature shapes.

Guide: "Custom accept loops" shows a hand-written loop with an inline
TLS handshake over `Server::serve_connection`; "Raw hyper" is what is
left for hyper-only knobs. Tests: plaintext `axum::serve` stamps
`PeerAddr`; max connection age retires an axum h2 connection; a
panicking axum handler yields 500 and the connection survives; the free
function hosts an `axum::Router` for a hand-written loop and reports
`Closed`.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BgcfmMV1ECyzhbqGvUV1a7
Signed-off-by: Ryan Brewster <rpb@anthropic.com>
… driver

Spawning `serve_connection`'s future on another runtime moves its
timers, HTTP/2 stream tasks and handlers, but a tokio socket stays on
the I/O driver of the runtime that created it. That runtime keeps
waking the connection and must outlive it, so a busy accepting runtime
still delays a connection spawned on another runtime. The guide's
accept loop now re-registers the socket on the target runtime
(`into_std`, then `TcpStream::from_std` inside the task) and spawns
with `JoinSet::spawn_on`, and the `serve_connection` docs say why. A
new test serves an HTTP/2 connection on a second runtime after the
accepting runtime has shut down, and checks that the handler ran there.

The shared driver, which `connectrpc::axum::serve` uses in place of
`axum::serve`, also had four problems:

- A panic while a response body was produced unwound through the
  connection task, so over HTTP/1.1 the `serve_connection` future
  panicked instead of reporting `CloseReason::Error`. Every response
  body is now wrapped so that such a panic is logged and becomes a body
  error: an HTTP/2 stream reset with `INTERNAL_ERROR`, or a closed
  HTTP/1.1 connection.
- With idle reaping on, a request stopped counting as in flight once
  its response head was returned, so a long streaming response could be
  closed as idle. The in-flight guard now lives in the response body.
- The accept loop retried `EMFILE` / `ENFILE` without pausing, spinning
  a core until a descriptor was freed. It now waits a second first, or
  until shutdown.
- HTTP/2 extended CONNECT was off, so HTTP/2 WebSockets failed.

The changelog covers these. The README and crate-docs feature tables
now say which `axum` items need `server` / `server-tls`, the `ServeTls`
deprecation names 0.10.0, and the changelog no longer describes the
unreleased `Server::serve_connection` as changed.

Signed-off-by: Iain McGinniss <309153+iainmcgin@users.noreply.github.com>
@iainmcgin
iainmcgin force-pushed the rpb/c-serve-connection branch from f4cfeac to 86f528e Compare September 18, 2026 05:49
@iainmcgin
iainmcgin marked this pull request as ready for review September 18, 2026 05:49

@iainmcgin iainmcgin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[claude code] Approving under the maintainers' agreement to review and merge each other's PRs. Rebased onto main after #307 merged; the added fix commits went through code, API, doc and deslop review.

@iainmcgin
iainmcgin enabled auto-merge September 18, 2026 05:49
@iainmcgin
iainmcgin added this pull request to the merge queue Sep 18, 2026
Merged via the queue into main with commit 7c35565 Sep 18, 2026
14 checks passed
@iainmcgin
iainmcgin deleted the rpb/c-serve-connection branch September 18, 2026 05:56
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.

2 participants