Skip to content

fix(guard): only echo rivet websocket subprotocol when client offers it#5307

Open
NathanFlurry wants to merge 1 commit into
mainfrom
fix-guard-ws-rivet-subprotocol
Open

fix(guard): only echo rivet websocket subprotocol when client offers it#5307
NathanFlurry wants to merge 1 commit into
mainfrom
fix-guard-ws-rivet-subprotocol

Conversation

@NathanFlurry

Copy link
Copy Markdown
Member

Problem

Browser WebSocket clients that connect to the actor gateway with a plain WebSocket (no rivet subprotocol) cannot connect. Two issues in the guard:

  1. Unsolicited subprotocol echo. The gateway unconditionally added Sec-WebSocket-Protocol: rivet to the handshake response (guard-core/proxy_service.rs, both the normal and error-proxy upgrade paths). Per RFC 6455 a server may only select a subprotocol the client offered, and browsers reject a handshake response that echoes an unsolicited one. RivetKit's own browser clients offer rivet, so they were unaffected, but any non-RivetKit raw client (e.g. tldraw's useSync) was rejected by the browser with:

    Error during WebSocket handshake: Response must not include 'Sec-WebSocket-Protocol' header if not present in request: rivet

  2. Mandatory subprotocol header on the query-gateway path. read_gateway_token_for_path_based (used by the ?rvt-method=... query routing path) required a Sec-WebSocket-Protocol header purely to look for an optional rivet_token.* entry, throwing missing_header when absent. The gateway token is optional (auth, when required, is enforced downstream), so a plain browser client with no token was rejected before the upgrade completed.

Fix

  • Echo Sec-WebSocket-Protocol: rivet only when the client actually offered it (new client_offered_rivet_subprotocol helper, applied at both upgrade sites).
  • Make read_gateway_token_for_path_based treat the subprotocol header as optional for WebSockets: return None (no token) when it is absent instead of erroring.

The query-gateway path already routes by URL (/gateway/{name}?rvt-namespace=...&rvt-method=getOrCreate&rvt-key=...), so no subprotocol is needed for routing. RivetKit clients that offer rivet are unchanged (they still get the echo).

Validation

Verified end-to-end against a local build of this branch: a tldraw multiplayer whiteboard (plain useSync browser WebSocket → query gateway → actor onWebSocket hosting TLSocketRoom) connects, and edits sync bidirectionally between two browser sessions with live presence. Before this change the handshake was rejected by the browser; after it, the WebSocket connects and routes to the actor.

🤖 Generated with Claude Code

@railway-app

railway-app Bot commented Jun 19, 2026

Copy link
Copy Markdown

🚅 Deployed to the rivet-pr-5307 environment in rivet-frontend

Service Status Web Updated (UTC)
kitchen-sink 😴 Sleeping (View Logs) Web Jun 19, 2026 at 2:38 am
website 😴 Sleeping (View Logs) Web Jun 19, 2026 at 2:38 am
frontend-inspector 😴 Sleeping (View Logs) Web Jun 19, 2026 at 2:36 am
frontend-cloud 😴 Sleeping (View Logs) Web Jun 19, 2026 at 2:34 am
ladle ✅ Success (View Logs) Web Jun 19, 2026 at 2:29 am
mcp-hub ✅ Success (View Logs) Web Jun 19, 2026 at 2:27 am

@claude

claude Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Code Review

Overview

This PR fixes two related WebSocket handshake bugs in the guard:

  1. The gateway unconditionally echoed Sec-WebSocket-Protocol: rivet even when the client did not offer it, violating RFC 6455 and causing browser rejection for plain WebSocket clients (e.g., tldraw's useSync).
  2. read_gateway_token_for_path_based required the Sec-WebSocket-Protocol header to be present just to look for an optional token entry, blocking plain WebSocket clients from connecting.

The fix is well-targeted and both changes are correct.


Correctness

  • RFC 6455 compliance: Using get_all() to iterate over all Sec-WebSocket-Protocol headers before checking comma-separated values is correct. Clients may send the header once with comma-separated values or multiple times.
  • Token optionality: Making the protocol header optional in read_gateway_token_for_path_based is safe because auth enforcement happens downstream. The change returns None (no token) rather than erroring, which is the right semantic.
  • Invalid UTF-8 handling: .to_str().ok() silently returns None for non-UTF-8 header values, which correctly treats an unparseable header as absent.

Issues

Comment style violates CLAUDE.md

CLAUDE.md says: "Never write multi-paragraph docstrings or multi-line comment blocks - one short line max."

The 7-line block comment above client_offered_rivet_subprotocol in proxy_service.rs violates this rule. The RFC 6455 constraint is non-obvious enough to warrant a comment, but it should be condensed to a single line, e.g. // RFC 6455: only select a subprotocol the client offered; browsers reject unsolicited echoes.

The same applies to the 3-line inline comment blocks at both call sites in proxy_service.rs. Consider trimming them to one line each.

No automated tests

The PR description validates the fix against a real local build, which is good for browser-specific behavior. However, there are no automated tests covering:

  • Subprotocol-echo suppression: a plain client gets no echo; a rivet client gets the echo.
  • The token-optional path in read_gateway_token_for_path_based for WebSockets without Sec-WebSocket-Protocol.

A unit test for client_offered_rivet_subprotocol would be straightforward and worth adding.


Minor Observations

  • The refactored read_gateway_token_for_path_based is cleaner and more idiomatic via chained and_then, an improvement over the earlier ok_or_else pattern.
  • The constant RIVET_WS_SUBPROTOCOL avoids repeating the string literal and keeps the two call sites in sync.
  • The non-path-based WebSocket routing (actor-id from the protocol header, around line 174 in mod.rs) still requires SEC_WEBSOCKET_PROTOCOL for actor identification. That is intentionally out of scope here, but worth noting: plain WebSocket clients can only use the path-based routing.

Summary

The fix is correct, minimal, and well-explained. Two things to address before merging:

  1. Condense the multi-line comment blocks to single lines per CLAUDE.md convention.
  2. Consider adding a unit test for client_offered_rivet_subprotocol and the optional-token path.

Generated with Claude Code

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