Skip to content

Polling transport: page stalls with no error after the server rejects an oversized CONNECT POST #7620

Closed

Description

@FarhanAliRaza

Summary

With transport="polling", a CONNECT packet larger than REFLEX_SOCKET_MAX_HTTP_BUFFER_SIZE (default 1 MB) stops the page. The client makes one reconnect attempt. The handshake GET of that attempt never completes. The page does not hydrate, no connection toast shows, and no further attempt is made in the 5–8 s observation windows.

The websocket transport does not have this problem. Found while reviewing #7605 (fix for #7486). It happens on main (4be8b4d) and on #7605.

Repro

rxconfig.py:

import reflex as rx

config = rx.Config(app_name="repro")

repro/repro.py:

import reflex as rx


class ReproState(rx.State):
    local: str = rx.LocalStorage("small", name="repro-local")


def index() -> rx.Component:
    return rx.vstack(
        rx.text(rx.cond(rx.State.is_hydrated, "true", "false"), id="hydrated"),
        rx.text(ReproState.local.length()),
    )


app = rx.App()
app.add_page(index)
  1. Start the app with REFLEX_TRANSPORT=polling set for both the frontend and the backend. The backend rejects the transport if only the frontend has it.
  2. Open the page and wait until #hydrated shows true.
  3. In the browser console, run localStorage.setItem('repro-local', 'x'.repeat(1_100_000)).
  4. Reload the page.

Observed

Chromium (Playwright, headless). Request log after the reload, times in ms:

 55  GET  handshake (no sid)        -> 200
 80  GET  poll (sid A)
189  POST CONNECT packet (sid A)    -> 400
191  GET  poll (sid A)              -> 200   (session closed; client logs disconnect "transport close")
357  GET  handshake (no sid)        -> never completes
  • #hydrated stays false. No toast shows, because only one connection error is recorded.
  • A direct curl "http://127.0.0.1:8000/_event/?EIO=4&transport=polling" handshake to the same backend answers in under 1 ms during the stall. The backend is not blocked.
  • During the stall, ss shows two browser connections to the backend with about 975 KB in the send queue: one ESTAB, one FIN-WAIT-1. The server answers 400 to the large POST without reading its body. The unsent body seems to block the browser's connections, so the next handshake request is not served.
  • On main the second attempt starts at once. On fix(state): back off when the connection closes before connecting #7605 it starts after the 200 ms backoff. The stall is the same.
  • I did not measure how long the stall lasts after 8 s. socket.io-client's connect timeout (20 s by default) may end it.

Expected

The client records each failed attempt, shows the connection toast, and keeps retrying with backoff, as it does with the websocket transport. The page hydrates after the storage is reduced.

Notes

  • A reverse proxy that answers 413 to the large POST, without the server seeing the body, does not stall. That case is handled in fix(state): back off when the connection closes before connecting #7605.
  • A possible direction: read (drain) the request body before the 400 response, or close the connection with the response, so the browser does not hold a half-sent request.

Activity

  1. linear-code commented on Oct 20, 2026

    @linear-code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions