defect
major
#29878
React UI does not react behind a proxy that buffers or refuses the window event stream
Users behind a content-inspecting corporate proxy (Zscaler, Netskope; seen at a customer site) cannot use any React UI of the engine. They cannot even log in. Found while analysing the tl-ai chat behind such proxies.
Problem
Observed
- The proxy holds a text/event-stream response until it ends, or forwards it only in large blocks (e.g. 16 KB), or recompresses it. The window stream react-api/events then delivers no event, or only the first burst.
- Every result of a React command arrives on this stream. The page does not react to any click. The login dialog is an SSE patch, so "Anmelden" does nothing.
- A proxy that refuses event streams closes the EventSource at once. sse-client.ts treats every closed stream as a lost session and reloads the page: an endless reload loop.
Expected
The React UI works behind such a proxy, with a short delay at most. A proxy that passes the stream keeps the stream (no change for them).
Reproduce
- Start an app with a React UI (e.g. com.top_logic.demo.react).
- Put a proxy in front of it that buffers text/event-stream until the response ends (or forwards it in 16 KB blocks, or answers it with 403).
- Open the app through the proxy and try to log in.
- Result: nothing happens after "Anmelden" (403: the page reloads without end).
Environment: master (8.0.0-alpha10 and later), com.top_logic.layout.react. Reproduced in a local docker proxy rig with 7 profiles (pass, buffer-full, buffer-16k, gzip, idle-30s, deny-sse, block-sse).
Cause
- SSEServlet / SSEUpdateQueue deliver the window's events only on the event stream. There is no other way for the client to get them.
- react-src/bridge/sse-client.ts cannot tell a stream that does not arrive or arrives partly. On onerror with a closed stream it reloads the page.
- A partly delivering proxy lets the initial full-state burst through, so a check "a first event arrived" is not sufficient: later small events stick in the proxy.
Proposal
- suggested SSEUpdateQueue numbers the events it writes (SSE id) and keeps them until they are read (bounded, cleared on connect and render).
- suggested New PollServlet at react-api/poll (async long poll, up to 10 s, after the last number the client saw). The first poll is set up like a connect and gets the full control state; later polls carry a window token instead of the session cookie, so polling does not keep the session alive.
- suggested The answer of a command carries the header X-TL-Last-Event (number of the last event written). The client compares it with its stream and polls when the stream stays behind.
- suggested The stream starts with 2 KB of padding and an immediate first event; the heartbeat comes every 15 s.
- suggested sse-client.ts changes to polling when no first event arrives within 3 s, when the stream is refused, when the heartbeat is missing, or when the events of a command do not arrive. A refused stream no longer reloads the page: the first poll tells a lost session (401/410, then a guarded reload) from a proxy that blocks streams. A tab that had to poll polls at once after a reload. expectEvents() is exported so that other code (e.g. a chat) can ask for the same check.
The change is purely additive: a new servlet in the web-fragment.xml of com.top_logic.layout.react, a new response header, no new configuration. No migration is necessary.
Done when
- Behind a proxy that buffers, forwards in blocks, recompresses or refuses the event stream, a user can log in and use the React UI.
- Behind a proxy that passes the stream, the client stays on the stream.
- A refused stream does not cause a reload loop; a lost session still reloads the page once.
- A test that fails on today's master shows that events are delivered by polling.