0.2.0: transport-agnostic protocol on tokio-tungstenite, plus axum adapter #1

Merged
selimaj-dev merged 1 commits from protocol-transport into master 2026-09-25 03:37:30 +00:00
Owner

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

## 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)
selimaj-dev added 1 commit 2026-09-25 03:36:17 +00:00
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]>
selimaj-dev merged commit c9df3b5717 into master 2026-09-25 03:37:30 +00:00
selimaj-dev deleted branch protocol-transport 2026-09-25 03:37:33 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: selimaj-dev/session-rs#1