fix(state): back off when the connection closes before connecting
Merged
FA
11d ago+177+177+012345678901234567890123456789-12-12-01234567890123456789
Summary
Fixes #7486. A page with more than 1 MB of browser storage reconnected hundreds of times per second, showed no error, and never hydrated. The cause is the reconnect logic, not the payload size:
- When the server drops the CONNECT packet (here, because it is over
REFLEX_SOCKET_MAX_HTTP_BUFFER_SIZE), socket.io-client emitsdisconnect("transport close"). The namespace never connected. Thedisconnecthandler reconnected at once and recorded no error. Now, whenwait_connectis still set, the close counts as a failed attempt: it records a connection error and retries with the incremental backoff (theconnect_errorpath). - The backoff read its attempt count from inside the
setConnectErrorsupdater. React runs that updater lazily when the component has pending work, so a retry can get a 0 ms delay. In a live app this hit the first retry in 1 of 21 measured retries. Change 1 alone stops the loop. The count now lives in theconnect()closure. It resets on connect and is capped at 10, as before. - A transport error before the namespace connects emits
connect_errorand thendisconnect. Thedisconnectreconnected at once and skipped the backoff. For example, a polling CONNECT that a proxy rejects with 413 looped every ~25 ms. Thedisconnectnow waits for the pending retry. A connect cancels the pending retry.
This also bounds limits that the client cannot know, for example a proxy that closes the socket on large frames.
Tests
state_js.test.mjs: a transport that closes before CONNECT retries with 200 ms, then 400 ms, and records an error. A transport error (connect_errorthendisconnect) schedules one retry and no immediate reconnect. A connect cancels the pending retry. The harness now appliessetConnectErrorsupdaters only on render, as React does with pending work. A connection that drops after it connected still reconnects at once.test_hydration_storage.py::test_refused_hydration_retries_with_backoff: the real server limit (1.1MLocalStoragevalue) and a proxy limit (a route that closes frames over 100 kB). Both show the connection toast, retry at least three times with growing gaps of more than 150 ms, and hydrate after the storage is cleared. Both cases fail onmain.
Relation to #7551
#7551 checks the CONNECT packet size on the client. That check does not cover the proxy case, so the loop stays there. A size check can still follow as an optional, clearer message for the known server limit.