Problem
In many corporate/enterprise network environments, HTTP proxies or firewalls block WebSocket (wss://) connections. This makes CloudCLI completely unusable for both the Chat interface and the Shell terminal, as both rely exclusively on WebSocket transport.
Affected scenarios:
- Corporate HTTP proxies that only allow standard HTTP/HTTPS traffic
- Firewalls with deep packet inspection that strip
Upgrade: websocket headers
- Restrictive network policies that whitelist only ports 80/443 with HTTP-only protocols
- SSE (Server-Sent Events) is also blocked in some environments
Proposed Solution
Implement an automatic HTTP Long Polling fallback that transparently substitutes WebSocket when connections fail. The design should:
1. Auto-detection & transparent fallback
- Track WebSocket connection failures (e.g., after 3 consecutive failures)
- Automatically switch to HTTP polling mode without user intervention
- No degradation for users with working WebSocket connections
2. Chat polling transport (/ws replacement)
POST /api/poll/connect — register a polling connection
GET /api/poll/messages — drain queued messages (with aggressive cache-busting headers)
POST /api/poll/send — send commands (claude-command, abort-session, etc.)
POST /api/poll/disconnect — cleanup
- Adaptive polling interval: ~100ms during activity, ~500ms when idle
- 10ms yield between message dispatches to prevent React setState batching issues
3. Shell polling transport (/shell replacement)
POST /api/poll/shell/connect — register shell polling + spawn PTY via handleShellConnection
GET /api/poll/shell/output — drain PTY output queue
POST /api/poll/shell/send — forward init/input/resize to PTY
POST /api/poll/shell/disconnect — cleanup
- Faster polling interval (~50ms active / ~200ms idle) for interactive terminal feel
- Server-side
FakeShellWs (EventEmitter) that mimics WebSocket API for handleShellConnection compatibility
4. Frontend monkey-patch approach
- Intercept
new WebSocket(url) calls for /ws?token= and /shell?token= URLs
- Replace with
PollingWebSocket / PollingShellWebSocket classes that use fetch() internally
- Maintain full WebSocket API compatibility (readyState, onopen, onmessage, onclose, send, close, addEventListener)
Implementation Notes
I have a working proof-of-concept deployed in a production environment behind an Nginx reverse proxy with SSL. Key learnings:
- Cache-busting is critical — Browser/proxy caching of poll responses causes stale data. Need
Cache-Control: no-store, unique ETag, and _t=Date.now() query params.
- Auth must use headers, not query params — Passing JWT tokens as URL query parameters causes
jwt malformed errors in some proxy configurations. Use Authorization: Bearer header instead.
- React setState batching — Dispatching multiple
onmessage events synchronously causes React to only render the last message. A small await setTimeout(10ms) between dispatches fixes this.
- Service Worker interference — SW caching of
index.html prevents users from getting updated polling code. Cache version bumping is needed.
Ideal Implementation Path
Rather than the monkey-patch approach (which works but is fragile), the proper implementation would be:
- Add polling transport as a first-class option in the React source code (e.g.,
usePollingWebSocket hook)
- Server-side polling endpoints as a separate Express router module
- Environment variable to force polling mode:
FORCE_POLLING=true
- Auto-detection built into the WebSocket connection hook with configurable threshold
Environment
- CloudCLI version: 1.25.2 (npm
@siteboon/claude-code-ui)
- Deployment: Linux server behind Nginx reverse proxy with SSL
- Corporate network: HTTP proxy blocking WebSocket and SSE
Submitted by @ShijunDeng — happy to contribute a PR if the maintainers are interested in this direction.
Problem
In many corporate/enterprise network environments, HTTP proxies or firewalls block WebSocket (wss://) connections. This makes CloudCLI completely unusable for both the Chat interface and the Shell terminal, as both rely exclusively on WebSocket transport.
Affected scenarios:
Upgrade: websocketheadersProposed Solution
Implement an automatic HTTP Long Polling fallback that transparently substitutes WebSocket when connections fail. The design should:
1. Auto-detection & transparent fallback
2. Chat polling transport (
/wsreplacement)POST /api/poll/connect— register a polling connectionGET /api/poll/messages— drain queued messages (with aggressive cache-busting headers)POST /api/poll/send— send commands (claude-command, abort-session, etc.)POST /api/poll/disconnect— cleanup3. Shell polling transport (
/shellreplacement)POST /api/poll/shell/connect— register shell polling + spawn PTY viahandleShellConnectionGET /api/poll/shell/output— drain PTY output queuePOST /api/poll/shell/send— forward init/input/resize to PTYPOST /api/poll/shell/disconnect— cleanupFakeShellWs(EventEmitter) that mimics WebSocket API forhandleShellConnectioncompatibility4. Frontend monkey-patch approach
new WebSocket(url)calls for/ws?token=and/shell?token=URLsPollingWebSocket/PollingShellWebSocketclasses that usefetch()internallyImplementation Notes
I have a working proof-of-concept deployed in a production environment behind an Nginx reverse proxy with SSL. Key learnings:
Cache-Control: no-store, uniqueETag, and_t=Date.now()query params.jwt malformederrors in some proxy configurations. UseAuthorization: Bearerheader instead.onmessageevents synchronously causes React to only render the last message. A smallawait setTimeout(10ms)between dispatches fixes this.index.htmlprevents users from getting updated polling code. Cache version bumping is needed.Ideal Implementation Path
Rather than the monkey-patch approach (which works but is fragile), the proper implementation would be:
usePollingWebSockethook)FORCE_POLLING=trueEnvironment
@siteboon/claude-code-ui)Submitted by @ShijunDeng — happy to contribute a PR if the maintainers are interested in this direction.