WebSocket passthrough broken — proxy_to_socket buffers responses and strips Upgrade header #35
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_router#35
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
proxy_to_socket()inserver/routes.rsdoes not support WebSocket connections. When a browser sends a WebSocket upgrade request through hero_router to a backend service (e.g., hero_collab_ui, hero_whiteboard_ui), the handshake fails with:Root Cause
Two issues in
proxy_to_socket()(routes.rs lines 537-565):1. Response body is fully buffered (line 541):
WebSocket requires a streaming bidirectional connection. Buffering the entire response body kills this.
2.
Upgradeheader is stripped from the response (line 556):The request side is fine — incoming headers including
Upgrade: websocketARE forwarded to the backend (lines 509-523). But the response never makes it back.Impact
This affects every Hero service that uses WebSocket on its ui.sock:
/ws/{channel_id}— real-time chat sync (per TECH_SPEC Section 5)/ws/{board_id}— real-time collaborative drawing/api/services/{name}/ptyand/api/jobs/{id}/pty— terminal PTYAll three services implement WebSocket handlers with
WebSocketUpgradeon their ui.sock, and use.with_upgrades()oraxum::serve()to support the protocol. The WebSocket pattern is the standard Hero real-time architecture — hero_collab's TECH_SPEC explicitly calls it the "proven dual-channel pattern" learned from hero_whiteboard.Proposed Fix
When an incoming request has
Upgrade: websocket+Connection: Upgradeheaders, use a bidirectional tunnel instead of the buffered request/response proxy:This is ~30-40 lines of code in a new branch alongside the existing
proxy_to_socket. The non-WebSocket path stays unchanged.Reference implementation
hero_proc_ui already implements this pattern internally (
run_pty_proxyin routes.rs lines 517-577) — it accepts the browser WebSocket on ui.sock, then opens a second WebSocket to rpc.sock viatokio_tungstenite::client_async. hero_router needs the same capability at the proxy level so services don't each need their own WebSocket proxy.Related
Closing — WebSocket support already exists in hero_router
After deeper investigation, hero_router already has WebSocket passthrough implemented:
is_ws_upgrade()(line 409) detectsConnection: upgrade+Upgrade: websocketws_proxy_inner()(line 424) resolves the backend socketproxy_ws_tunnel()(line 466) does bidirectional raw tunnel viahyper::upgrade::on()proxy_to_socketpathThe
Upgradeheader stripping I reported (line 556) only applies to regular HTTP responses — WebSocket requests never reach that code path.The actual issue is likely in hero_collab_ui's WebSocket handling or the version of hero_router deployed locally. Will investigate further on the hero_collab side.
Sorry for the false report!