FIX: Stream Auth through protocol bug fixed#1463
FIX: Stream Auth through protocol bug fixed#1463Atlas1225 wants to merge 1 commit intoiii-hq:mainfrom
Conversation
…d not terminate the stream automatically (by return the Authorization protocol)
|
Someone is attempting to deploy a commit to the motia Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughUpdated the WebSocket handler to conditionally request the "Authorization" subprotocol during handshake. The implementation checks the Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@engine/src/workers/stream/stream.rs`:
- Around line 77-81: The code that sets has_authorization_protocol currently
uses headers.get(...) which only returns the first Sec-WebSocket-Protocol header
instance; change the logic to iterate over
headers.get_all("sec-websocket-protocol"), for each header value try
to_str().ok(), split by ',' and trim each token, and set
has_authorization_protocol = true if any token equals "Authorization"
(short-circuiting once found). Update the usage around the
has_authorization_protocol variable to rely on this aggregated check so repeated
header instances or comma-separated lists are both handled.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 75443384-1c52-4a49-958a-228ef7a00a3f
📒 Files selected for processing (1)
engine/src/workers/stream/stream.rs
| let has_authorization_protocol = headers | ||
| .get("sec-websocket-protocol") | ||
| .and_then(|v| v.to_str().ok()) | ||
| .map(|s| s.split(',').any(|p| p.trim() == "Authorization")) | ||
| .unwrap_or(false); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the file structure and the specific lines mentioned
head -n 100 engine/src/workers/stream/stream.rs | tail -n 50Repository: iii-hq/iii
Length of output: 1505
🏁 Script executed:
# Check the full imports to understand what HeaderMap is being used
head -n 30 engine/src/workers/stream/stream.rsRepository: iii-hq/iii
Length of output: 949
🏁 Script executed:
# Look at the context around lines 77-81 more carefully
sed -n '70,95p' engine/src/workers/stream/stream.rsRepository: iii-hq/iii
Length of output: 890
🏁 Script executed:
# Search for HeaderMap imports and types
rg "use.*HeaderMap\|use.*headers" engine/src/workers/stream/stream.rsRepository: iii-hq/iii
Length of output: 36
🏁 Script executed:
# Check if there are any tests for this specific logic
rg -A 10 "sec-websocket-protocol\|SecWebSocketProtocol" engine/src/workers/stream/Repository: iii-hq/iii
Length of output: 36
🏁 Script executed:
# Check if axum's HeaderMap has get_all method and what it supports
rg "HeaderMap" engine/src/workers/stream/stream.rs -A 5 -B 5Repository: iii-hq/iii
Length of output: 559
🏁 Script executed:
# Look for Cargo.toml to see axum version
fd Cargo.toml engine/ | head -5 | xargs head -n 50Repository: iii-hq/iii
Length of output: 1106
🏁 Script executed:
web_search
axum http HeaderMap get_all method APIRepository: iii-hq/iii
Length of output: 147
🏁 Script executed:
web_search
HTTP Sec-WebSocket-Protocol header multiple instances RFC 6455Repository: iii-hq/iii
Length of output: 147
🏁 Script executed:
# Check if there are any SDK or documentation files that document this protocol behavior
find . -type f \( -name "*.md" -o -name "*.rs" \) | xargs rg -l "Sec-WebSocket-Protocol|sec-websocket-protocol" | grep -E "sdk/|docs/" | head -10Repository: iii-hq/iii
Length of output: 36
🏁 Script executed:
# Check the actual axum version in dependencies
rg "^axum" engine/Cargo.tomlRepository: iii-hq/iii
Length of output: 109
🌐 Web query:
axum 0.8 HeaderMap get_all method documentation
💡 Result:
In axum 0.8, HeaderMap is re-exported from the http crate (version 1.0+). The get_all method returns a view over all values associated with a given header key without allocations. Signature: pub fn get_all(&self, key: K) -> GetAll<'_, T> where K: AsHeaderName Description: Returns a view of all values associated with a key. The returned GetAll allows iterating the values in insertion order. Returns None if no values are associated with the key. Example: let mut map = HeaderMap::new; map.insert(HOST, "hello".parse.unwrap); map.append(HOST, "goodbye".parse.unwrap); let view = map.get_all("host"); let mut iter = view.iter; assert_eq!(&"hello", iter.next.unwrap); assert_eq!(&"goodbye", iter.next.unwrap); assert!(iter.next.is_none); GetAll provides iter which returns ValueIter<'a, T>, an iterator over &'a T in insertion order. This API is unchanged from previous versions and matches the standard http::header::HeaderMap documentation.
Citations:
- 1: https://docs.rs/ajars_axum/latest/ajars_axum/axum/http/header/struct.HeaderMap.html
- 2: https://docs.rs/ajars_axum/latest/x86_64-pc-windows-msvc/ajars_axum/axum/http/header/struct.GetAll.html
- 3: https://docs.rs/dioxus/latest/dioxus/prelude/dioxus_server/axum/http/header/struct.HeaderMap.html
- 4: https://docs.rs/crate/axum/^0.8.6
- 5: https://docs.rs/crate/axum/latest
🌐 Web query:
HTTP RFC 6455 Sec-WebSocket-Protocol header multiple values repeated header
💡 Result:
In RFC 6455 (The WebSocket Protocol), the Sec-WebSocket-Protocol header handles multiple subprotocol values differently for client requests and server responses. For client requests (Sec-WebSocket-Protocol-Client): The ABNF is 1#token, allowing multiple values either as a comma-separated list in a single header (e.g., Sec-WebSocket-Protocol: chat, superchat) or as repeated headers (e.g., Sec-WebSocket-Protocol: chat Sec-WebSocket-Protocol: superchat). This lists subprotocols in order of preference. The server selects at most one from this list. For server responses (Sec-WebSocket-Protocol-Server): The ABNF is token (a single token), so only one value is permitted—exactly one subprotocol selected from the client's list, sent in a single header instance (e.g., Sec-WebSocket-Protocol: chat). Sending multiple values or repeated headers is invalid and causes handshake failure, as confirmed by implementations and docs stating "the server can't send more than one Sec-WebSocket-Protocol header" and "server must return exactly one value". This contrasts with Sec-WebSocket-Extensions, which explicitly allows multiple instances or comma lists for extensions. HTTP/1.1 semantics support repeated headers for list-valued fields, but WebSocket restricts the server response to a single token. No updates in later RFCs (e.g., 7936, 8441) change this behavior.
Citations:
- 1: https://datatracker.ietf.org/doc/html/rfc6455
- 2: https://rfc-editor.org/rfc/rfc6455.html
- 3: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-WebSocket-Protocol
- 4: Incorrectly returning multiple
Sec-WebSocket-Protocolheaders h3js/crossws#141 - 5: https://www.websocket.org/reference/headers/
- 6: https://rfcinfo.com/rfc-6455/OpeningHandshake
- 7: multiple alternative protocols not parsed correctly warmcat/libwebsockets#147
- 8: https://dev.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-WebSocket-Extensions
🏁 Script executed:
# Check the dependencies to see what http crate version is used
rg "^http\s*=" engine/Cargo.tomlRepository: iii-hq/iii
Length of output: 36
🏁 Script executed:
# Look for any existing patterns in the codebase of handling multiple headers
rg "get_all\|iter\(\)" engine/src/ | grep -i header | head -20Repository: iii-hq/iii
Length of output: 36
🏁 Script executed:
# Check for cargo fmt compliance by looking at the file formatting
head -n 90 engine/src/workers/stream/stream.rs | tail -n 20Repository: iii-hq/iii
Length of output: 642
🏁 Script executed:
# Verify cargo fmt compliance - check if file passes formatting
cd engine && cargo fmt --check -- src/workers/stream/stream.rs 2>&1 || trueRepository: iii-hq/iii
Length of output: 300
🏁 Script executed:
# Search more broadly for SDK changes or documentation that might reference WebSocket protocol handling
find . -type f -name "*.rs" -path "*/sdk/*" | xargs rg -l "protocol\|subprotocol" 2>/dev/null | head -5Repository: iii-hq/iii
Length of output: 36
🏁 Script executed:
# Check if there are framework-level references to WebSocket subprotocols
find . -type f \( -name "*.md" -o -name "*.rs" \) -path "*/frameworks/*" | xargs rg -l "WebSocket\|subprotocol" 2>/dev/null | head -5Repository: iii-hq/iii
Length of output: 36
🏁 Script executed:
# Check if there are any WebSocket-related tests or integration tests that might show expected behavior
find . -type f -name "*.rs" | xargs rg -l "WebSocketUpgrade\|ws_handler\|protocols" | grep test | head -5Repository: iii-hq/iii
Length of output: 36
🏁 Script executed:
# Look for any protocol-related documentation or comments in the codebase
rg "Authorization.*protocol\|protocol.*Authorization" engine/src/ -B 2 -A 2Repository: iii-hq/iii
Length of output: 36
Handle all Sec-WebSocket-Protocol header instances, not just one.
RFC 6455 allows clients to send Sec-WebSocket-Protocol as either comma-separated values or repeated header instances. The current code using headers.get() retrieves only the first header value and will miss the Authorization protocol if it appears in a subsequent repeated header, causing protocol negotiation to fail.
Use headers.get_all() to iterate over all header instances:
Proposed fix
- let has_authorization_protocol = headers
- .get("sec-websocket-protocol")
- .and_then(|v| v.to_str().ok())
- .map(|s| s.split(',').any(|p| p.trim() == "Authorization"))
- .unwrap_or(false);
+ let has_authorization_protocol = headers
+ .get_all("sec-websocket-protocol")
+ .iter()
+ .filter_map(|v| v.to_str().ok())
+ .flat_map(|s| s.split(','))
+ .any(|p| p.trim() == "Authorization");📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let has_authorization_protocol = headers | |
| .get("sec-websocket-protocol") | |
| .and_then(|v| v.to_str().ok()) | |
| .map(|s| s.split(',').any(|p| p.trim() == "Authorization")) | |
| .unwrap_or(false); | |
| let has_authorization_protocol = headers | |
| .get_all("sec-websocket-protocol") | |
| .iter() | |
| .filter_map(|v| v.to_str().ok()) | |
| .flat_map(|s| s.split(',')) | |
| .any(|p| p.trim() == "Authorization"); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@engine/src/workers/stream/stream.rs` around lines 77 - 81, The code that sets
has_authorization_protocol currently uses headers.get(...) which only returns
the first Sec-WebSocket-Protocol header instance; change the logic to iterate
over headers.get_all("sec-websocket-protocol"), for each header value try
to_str().ok(), split by ',' and trim each token, and set
has_authorization_protocol = true if any token equals "Authorization"
(short-circuiting once found). Update the usage around the
has_authorization_protocol variable to rely on this aggregated check so repeated
header instances or comma-separated lists are both handled.
Per RFC 6455, if a WebSocket client sends a Sec-WebSocket-Protocol header during the handshake, the server must include a matching Sec-WebSocket-Protocol in its response — or the browser will immediately close the connection with code 1006.
The iii-stream worker was not echoing back the requested subprotocol, which made it impossible for browser clients to use the Sec-WebSocket-Protocol header as an authentication transport. The connection was silently dropped on every attempt.
FIX:
Based on your documentation the motia client in front should pass the token in websocket with this format
HEADER: Sec-WebSocket-Protocol: Authorization, <jwt>.
The server must echo: HEADER Sec-WebSocket-Protocol: Authorization. Without that, the ws is terminated automatically.
The server extract the token from the header value in the authenticateStream function for verification.
Summary by CodeRabbit