Repository navigation
fix(realtime): restore polling→upgrade transport so live displays update - #2944
Merged
Merged
Conversation
All four socket.io clients pinned transports:['websocket'], forcing a cold websocket-only handshake that the prod approuter rejects with engine.io TRANSPORT_HANDSHAKE_ERROR (code 3) — the socket never connects, so event/kiosk displays stop updating live and only refresh when the browser is reloaded (the reload triggers the unrelated getEventBuckets REST fetch). Confirmed in prod cf logs on the direct .cfapps host (not Akamai): every GET /socket.io/?EIO=4&transport=websocket returns 400, on both srv instances. The websocket-only pin (#2306) dodged a multi-instance polling-POST 400, but #2524 fixed that properly via JSESSIONID Gorouter affinity + the socket.io Redis adapter, so polling→upgrade is instance-safe again. Reverting to the socket.io default (polling, then auto-upgrade to websocket where it succeeds) restores live updates and degrades gracefully to polling where upgrade fails. Clients fixed: display-app, event-display, gameboard, app-space.
This was referenced Oct 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Event/kiosk displays (display-app, event-display, gameboard, app-space) stopped updating live — they only refreshed when the browser was manually reloaded. Root cause: all four socket.io clients pinned
transports: ['websocket'], forcing a cold websocket-only handshake that the prod approuter rejects with engine.ioTRANSPORT_HANDSHAKE_ERROR(code 3). The socket never connects, socompletionTick/tutorialCompletedevents never arrive. The manual-refresh "fix" is the unrelatedgetEventBucketsREST fetch.This is server-side, not Akamai — confirmed in live
cf logs tutorials-srvon the direct.cfappshost (CDN bypassed): everyGET /socket.io/?EIO=4&transport=websocketreturns400, on both srv instances.Fix
Revert the four clients to socket.io's default
transports: ['polling', 'websocket']. Polling handshake is a normal HTTP GET/POST the approuter proxies fine; socket.io then auto-upgrades to websocket where it succeeds and silently stays on polling where it doesn't.app/display-app/src/event-stream.ts(the reported kiosk)hugo-apps/src/event-display/useEventStream.tshugo-apps/src/gameboard/useGameboardStream.tshugo-apps/src/app-space/useRealtimeProgress.tsWhy the old pin is obsolete
The websocket-only pin came from #2306 to dodge a multi-instance polling-POST 400 (follow-up POST hit a different srv instance lacking the
sid). That was real then — but #2524 fixed it properly: JSESSIONID Gorouter affinity (srv/server.js) pins the handshake to one instance, and@socket.io/redis-adapter(tutorials-redis-websocket, bound) bridges cross-instance fan-out. So polling→upgrade is instance-safe again, and the pin now only causes the cold-upgrade 400.Mechanism (for reviewers)
engine.io raises
TRANSPORT_HANDSHAKE_ERRORatengine.io/build/server.js:148—if (transport === "websocket" && !upgrade)— when atransport=websockethandshake with no priorsidreaches the plain-HTTP handler instead of arriving as a real HTTP Upgrade. Websocket-only with no polling pre-handshake forces exactly that path.Verification
tsc --noEmit: clean (only the pre-existingstyle.cssside-effect-import stub).connectedand the counter ticks without refresh. Oracle:test/load/scenarios/05-websocket-handshake.js.Test plan
/display-app/?eventId=<live>&goalTarget=…, confirm noTRANSPORT_HANDSHAKE_ERRORincf logsand the counter increments live without reloadcf logsshows a successful upgrade (101) or a sustained polling session, not 400s