This removes the hand-rolled WebSocket implementation (src/ws/). Session becomes a protocol layer over any Sink<Frame> / Stream<Item = Result<Frame, E>> pair, with adapters behind features:
Feature
Default
Provides
server
yes
SessionServer on tokio-tungstenite, with a configurable ServerConfig (1 MiB message/frame limit, 5 s handshake timeout)
client
yes
Session::connect(url)
rustls / native-tls
no
wss:// for the client
axum
no
Session::from_axum(socket), so a session can share a router with HTTP routes such as /health
Session::from_transport and Session::from_tungstenite are public for custom setups, for example a TLS acceptor.
The JSON wire format is unchanged. Peers on 0.1.x, and session-java, keep working.
Fixes
Unbounded allocation:read_frame allocated whatever 64-bit length a peer claimed, so one ~14-byte frame header sent before auth could abort the process or exhaust memory. Sizes are now bounded by tungstenite's limits.
Lost responses:request subscribed for its response after sending, so a fast response could be missed and the request hung forever. Pending requests are now registered first and fail with ConnectionClosed when the session closes.
Handlers registered late:session_loop started the receiver before on_conn registered handlers, so an early request could be silently dropped. The receiver now starts after on_conn returns.
Server exits on accept error:session_loop returned on any accept() error, such as running out of file descriptors. It now logs the error, backs off and continues.
Leaked sessions:.unwrap() / .expect() in the receive loop could panic (an unsolicited response, or a disconnect mid-reply), which skipped on_close and leaked the session. on_close now runs exactly once, and a handler panic fails only its own request.
Silent failures: an unknown method or undeserializable data produced no reply, so the peer waited forever. It now gets an errorresponse.
Slow handlers blocked the socket: they stalled pongs and responses. Reading and dispatching are now separate tasks. Requests are still handled in arrival order.
Fragmented messages: the last fragment was dropped. tungstenite now reassembles messages.
The Saturn server compiles against this branch with no code changes.
Testing
tests/protocol.rs has 13 integration tests covering:
request/response/error round trips
200 concurrent requests
exact wire format
unknown methods and invalid data
handler panics
a handler requesting from its peer
notifications
close and on_close semantics
request_timeout
an oversized message
a raw frame header claiming 2^62 bytes
ping timeout
the axum adapter next to /health
All 13 tests passed 15 times in a row with cargo test --all-features.
Clippy is clean, every feature combination builds on its own, and cargo publish --dry-run passes.
I ran the Saturn server against this branch with a Python websockets client. get_player, a failed auth, set_cloak and an unknown method all returned correct responses. A 2 MiB message closed only that connection.
After merging
Publish 0.2.0 to crates.io.
Bump the Saturn server to session-rs = "0.2".
session-java has the same send-then-register race in request, which is worth fixing in its next release.
## Summary
This removes the hand-rolled WebSocket implementation (`src/ws/`). `Session` becomes a protocol layer over any `Sink<Frame>` / `Stream<Item = Result<Frame, E>>` pair, with adapters behind features:
| Feature | Default | Provides |
| --- | --- | --- |
| `server` | yes | `SessionServer` on tokio-tungstenite, with a configurable `ServerConfig` (1 MiB message/frame limit, 5 s handshake timeout) |
| `client` | yes | `Session::connect(url)` |
| `rustls` / `native-tls` | no | `wss://` for the client |
| `axum` | no | `Session::from_axum(socket)`, so a session can share a router with HTTP routes such as `/health` |
`Session::from_transport` and `Session::from_tungstenite` are public for custom setups, for example a TLS acceptor.
**The JSON wire format is unchanged.** Peers on 0.1.x, and `session-java`, keep working.
## Fixes
- **Unbounded allocation:** `read_frame` allocated whatever 64-bit length a peer claimed, so one ~14-byte frame header sent before auth could abort the process or exhaust memory. Sizes are now bounded by tungstenite's limits.
- **Lost responses:** `request` subscribed for its response *after* sending, so a fast response could be missed and the request hung forever. Pending requests are now registered first and fail with `ConnectionClosed` when the session closes.
- **Handlers registered late:** `session_loop` started the receiver before `on_conn` registered handlers, so an early request could be silently dropped. The receiver now starts after `on_conn` returns.
- **Server exits on accept error:** `session_loop` returned on any `accept()` error, such as running out of file descriptors. It now logs the error, backs off and continues.
- **Leaked sessions:** `.unwrap()` / `.expect()` in the receive loop could panic (an unsolicited response, or a disconnect mid-reply), which skipped `on_close` and leaked the session. `on_close` now runs exactly once, and a handler panic fails only its own request.
- **Silent failures:** an unknown method or undeserializable data produced no reply, so the peer waited forever. It now gets an `errorresponse`.
- **Slow handlers blocked the socket:** they stalled pongs and responses. Reading and dispatching are now separate tasks. Requests are still handled in arrival order.
- **Fragmented messages:** the last fragment was dropped. tungstenite now reassembles messages.
## API changes (0.1 → 0.2)
- `Session::connect(addr, path)` becomes `Session::connect(url)`.
- The `ws` module and `Session.ws` are removed. `Error::WebSocket` / `Error::RecvError` are replaced by `Transport`, `ConnectionClosed` and `Timeout`.
- The standalone server no longer answers a plain HTTP GET with `200 OK`. Use the axum adapter for HTTP routes.
- New: `on_notification`, `request_timeout`, `closed()`, `is_closed()`, `id()`, `SessionServer::{from_listener, with_config, local_addr}`.
The Saturn server compiles against this branch with no code changes.
## Testing
- `tests/protocol.rs` has 13 integration tests covering:
- request/response/error round trips
- 200 concurrent requests
- exact wire format
- unknown methods and invalid data
- handler panics
- a handler requesting from its peer
- notifications
- close and `on_close` semantics
- `request_timeout`
- an oversized message
- a raw frame header claiming 2^62 bytes
- ping timeout
- the axum adapter next to `/health`
- All 13 tests passed 15 times in a row with `cargo test --all-features`.
- Clippy is clean, every feature combination builds on its own, and `cargo publish --dry-run` passes.
- I ran the Saturn server against this branch with a Python `websockets` client. `get_player`, a failed `auth`, `set_cloak` and an unknown method all returned correct responses. A 2 MiB message closed only that connection.
## After merging
- Publish 0.2.0 to crates.io.
- Bump the Saturn server to `session-rs = "0.2"`.
- `session-java` has the same send-then-register race in `request`, which is worth fixing in its next release.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Replace the hand-rolled WebSocket implementation with a transport
boundary (Frame over any Sink/Stream) plus adapters for tokio-tungstenite
(server/client features) and axum (axum feature). The JSON wire format
is unchanged, so existing peers keep working.
Fixes:
- unbounded frame lengths were allocated up front; the server now
enforces message/frame size limits (1 MiB default)
- responses arriving before `request` subscribed were lost
- an accept() error ended `session_loop`
- requests sent right after connecting could arrive before handlers
were registered; the receiver now starts after `on_conn` returns
- panics in the receive loop skipped `on_close` and leaked sessions;
`on_close` now runs exactly once and handler panics fail only their
request
- unknown methods and invalid data got no reply; they now get an
error response
- slow handlers blocked pongs and responses
Adds `on_notification`, `request_timeout`, `closed`, `is_closed`, `id`,
`ServerConfig`, `from_transport`, `from_tungstenite` and `from_axum`,
integration tests, and an axum example. Bumps to 0.2.0 since
`Session::connect` now takes a URL and the `ws` module is removed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
This removes the hand-rolled WebSocket implementation (
src/ws/).Sessionbecomes a protocol layer over anySink<Frame>/Stream<Item = Result<Frame, E>>pair, with adapters behind features:serverSessionServeron tokio-tungstenite, with a configurableServerConfig(1 MiB message/frame limit, 5 s handshake timeout)clientSession::connect(url)rustls/native-tlswss://for the clientaxumSession::from_axum(socket), so a session can share a router with HTTP routes such as/healthSession::from_transportandSession::from_tungsteniteare public for custom setups, for example a TLS acceptor.The JSON wire format is unchanged. Peers on 0.1.x, and
session-java, keep working.Fixes
read_frameallocated whatever 64-bit length a peer claimed, so one ~14-byte frame header sent before auth could abort the process or exhaust memory. Sizes are now bounded by tungstenite's limits.requestsubscribed for its response after sending, so a fast response could be missed and the request hung forever. Pending requests are now registered first and fail withConnectionClosedwhen the session closes.session_loopstarted the receiver beforeon_connregistered handlers, so an early request could be silently dropped. The receiver now starts afteron_connreturns.session_loopreturned on anyaccept()error, such as running out of file descriptors. It now logs the error, backs off and continues..unwrap()/.expect()in the receive loop could panic (an unsolicited response, or a disconnect mid-reply), which skippedon_closeand leaked the session.on_closenow runs exactly once, and a handler panic fails only its own request.errorresponse.API changes (0.1 → 0.2)
Session::connect(addr, path)becomesSession::connect(url).wsmodule andSession.wsare removed.Error::WebSocket/Error::RecvErrorare replaced byTransport,ConnectionClosedandTimeout.200 OK. Use the axum adapter for HTTP routes.on_notification,request_timeout,closed(),is_closed(),id(),SessionServer::{from_listener, with_config, local_addr}.The Saturn server compiles against this branch with no code changes.
Testing
tests/protocol.rshas 13 integration tests covering:on_closesemanticsrequest_timeout/healthcargo test --all-features.cargo publish --dry-runpasses.websocketsclient.get_player, a failedauth,set_cloakand an unknown method all returned correct responses. A 2 MiB message closed only that connection.After merging
session-rs = "0.2".session-javahas the same send-then-register race inrequest, which is worth fixing in its next release.🤖 Generated with Claude Code