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

Merged
AN
2d ago
+309-1

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