Skip to content

fix: drain oversized polling POST bodies so a rejected connection event cannot stall the page - #7621

Merged
masenf merged 9 commits into
reflex-dev:mainfrom
anishgurjar:fix-drain-oversized-polling-post
Oct 27, 2026
Merged

masenf merged 9 commits into
reflex-dev:mainfrom
anishgurjar:fix-drain-oversized-polling-post

Conversation

@anishgurjar

@anishgurjar anishgurjar commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #7620.

With transport="polling", when the client sends a CONNECT packet larger than REFLEX_SOCKET_MAX_HTTP_BUFFER_SIZE (default 1 MB), engine.io compares Content-Length against max_http_buffer_size and answers 400 without reading the request body. The browser is still sending at that point, and the unanswered remainder wedges its connection pool, so the reconnect handshake is never served: the page stalls un-hydrated with no connection toast and no further retry.

This wraps the socket ASGI app in a small middleware (_DrainOversizedBodyMiddleware) that reads an oversized POST body to completion (discarding the bytes) before engine.io sees the request. The 400 then reaches a client that has finished sending, so it records the failure and retries with backoff, matching the websocket transport's behavior. Requests within the size limit pass through untouched.

Tests

New tests in tests/units/test_app.py drive a real engine.io server at the ASGI level (a parked long-poll mirrors the browser's in-flight poll, which the server needs in order to deliver its CLOSE packet when the session is rejected):

  • test_engineio_rejects_oversized_polling_post_without_draining_body documents the dependency behavior being worked around (400 with the body unread).
  • test_drain_oversized_body_middleware_drains_before_engineio_rejects asserts the body is fully drained, the 400 still returns, and a fresh handshake is served afterwards.
  • test_drain_oversized_body_middleware_passes_regular_requests_through asserts in-limit POSTs and GETs pass through untouched.

pytest tests/units/test_app.py: 197 passed. ruff check / ruff format --check clean.

cc @masenf

View guided diff

@anishgurjar
anishgurjar requested a review from a team as a code owner October 20, 2026 20:29

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 3 files

View guided diff | Re-trigger cubic

@greptile-apps

greptile-apps Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The reviewed changes appear safe to merge.

Summary

The PR drains oversized polling POST bodies before returning 400, then closes the rejected session without waiting for another poll.

  • Oversized polling requests finish cleanly so the browser can reconnect.

Reviews (5) · Last reviewed commit: "Decode the drained POST's query string a..." · Reviewed by Greptile

Comment thread tests/units/test_app.py Outdated
Comment thread reflex/app.py Outdated
@masenf masenf added the bug Something isn't working label Oct 21, 2026 — with ChatGPT Codex Connector
@FarhanAliRaza FarhanAliRaza added the on deck PRs lined up to review / merge next label Oct 22, 2026
@codspeed

codspeed Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

1 warning

⚠️ 19 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

✅ 148 untouched benchmarks


Comparing anishgurjar:fix-drain-oversized-polling-post (304bdab) with main (9c64f1e)

Open in CodSpeed

@FarhanAliRaza FarhanAliRaza left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See inline comments.


Generated by Claude Code

Comment thread reflex/app.py Outdated
Comment thread tests/units/test_app.py Outdated
After the drain, answer the 400 from _DrainOversizedBodyMiddleware itself
and close the engine.io session with socket.close(wait=False) instead of
delegating to engine.io. engine.io rejects by disconnecting the session,
whose close(wait=True) blocks in queue.join() until a poll GET consumes
the CLOSE packet; the engine.io client races the CONNECT POST against the
first poll GET, so with no poll parked the POST response was held forever
and later polls got 'Session is disconnected'. The CLOSE packet still
reaches a parked poll, but a missing poll no longer holds the response.

Add a no-parked-poll regression test under asyncio.wait_for; it times out
on the unpatched middleware and passes with the fix.
@greptile-apps

greptile-apps Bot commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Want your agent to iterate on Greptile's feedback? Try greploops.

@anishgurjar

Copy link
Copy Markdown
Contributor Author

The 11 workflows on the new head are awaiting maintainer approval. Could you approve the CI run? Local tests pass, but GitHub CI hasn't verified this head yet.

@anishgurjar

Copy link
Copy Markdown
Contributor Author

Both points addressed in 762788c. The middleware drains the oversized POST, returns 400 itself, and closes the engine.io socket with wait=False. The new no-parked-poll test times out on the previous revision and passes now. Local checks pass, and full CI is green on this head (166 passed, 3 skipped).

- Take the engine.io server as a required argument and read its buffer
  limit, instead of a separate max_size and an optional eio.
- Drain with a single more_body loop, read Content-Length through
  starlette Headers, and drop the ValueError and broad exception guards
  (servers validate Content-Length; engine.io logs handler errors).
- Import engineio only for type checking.
- Extract the handshake and oversized POST test helpers, and add a test
  that drives an oversized POST through the App's event endpoint.
- Describe the remaining user-facing case in the news fragment: reflex-dev#7551
  stops oversized hydrate packets in the client, so the stall now comes
  from oversized events.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 3 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread reflex/app.py Outdated
Comment thread news/+drain-oversized-polling-post.bugfix.md Outdated
engine.io treats a query string that is not UTF-8 as empty. A strict
decode raised after the 400 was sent, so the session was not closed.
Also word the news fragment as 'can reconnect': the client's reconnect
policy controls the timing.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 3 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread tests/units/test_app.py
@masenf
masenf merged commit 3919828 into reflex-dev:main Oct 27, 2026
169 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working on deck PRs lined up to review / merge next

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants