fix(state): back off when the connection closes before connecting

Merged
FA
11d ago
+177-12

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:

  1. When the server drops the CONNECT packet (here, because it is over REFLEX_SOCKET_MAX_HTTP_BUFFER_SIZE), socket.io-client emits disconnect("transport close"). The namespace never connected. The disconnect handler reconnected at once and recorded no error. Now, when wait_connect is still set, the close counts as a failed attempt: it records a connection error and retries with the incremental backoff (the connect_error path).
  2. The backoff read its attempt count from inside the setConnectErrors updater. 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 the connect() closure. It resets on connect and is capped at 10, as before.
  3. A transport error before the namespace connects emits connect_error and then disconnect. The disconnect reconnected at once and skipped the backoff. For example, a polling CONNECT that a proxy rejects with 413 looped every ~25 ms. The disconnect now 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_error then disconnect) schedules one retry and no immediate reconnect. A connect cancels the pending retry. The harness now applies setConnectErrors updaters 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.1M LocalStorage value) 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 on main.

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.