fix(login): rate-limit no longer latches a phantom offline on the login screen #255
Reference in New Issue
Block a user
Delete Branch "feature/login-rate-limit-offline"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Symptom
A fresh email login on the PWA: request a code, mistype it once (correct red "invalid code" toast), then enter the correct code — and the app claims you are offline. Buttons disable for ~20 s, and the state is unrecoverable: it survives a full PWA close/reopen + a fresh code, even though the network is fine the whole time.
Root cause
The gateway email class (
auth.email.request+auth.email.login, keyed per client IP,5/10 min, burst 2) trips on the third event. In the naturalrequest code → wrong code → correct codesequence that third event is the correct-code login, so the gateway answersResourceExhausted.The client maps that to
'rate_limited', whichretry.tsclassified as retryable + a connection code, soexec()calledreportOffline(). That pushes the net-state machine intoconnecting, whose recovery probe is an authenticatedprofile.get— but the login screen has no session, so the probe can never succeed and the machine latches intoofflineNoNetwork. The transport kill switch (assertOnline) then refuses the very login that would fix it. Because the IP's email bucket refills only 1 token / 120 s, every fresh attempt after a restart is rate-limited again → offline again.Fix — client (narrow: the trigger)
retry.tsno longer marks'rate_limited'retryable or a connection code, soexec()never reports it offline; it surfaces as the existingerror.rate_limited"slow down" message.Fix — server
2 → 4so the honestrequest + a mistyped code + the correct oneis not throttled mid-login. Defence-in-depth over the backend's own per-code guards (emailCodeMaxAttempts = 5+ 15-min TTL + per-recipient send throttle), which remain the real brute-force barrier.Tests
retry.test.ts: rate-limit classification updated to the new semantics (not retried, not a connection code).ratelimit_test.go: a regression guard asserts the honest three-event email flow passes under the default policy (so the burst can't be silently re-tightened below the login sequence).gateway/README.mdrate-limit note updated.Known residual (deferred by the owner)
The net-state recovery probe is session-gated, so any real transport failure on the session-less login/confirm screens still cannot self-heal (it just no longer fires spuriously on a rate-limit). Left as a follow-up.
Verification
go build/go vet/gofmtclean; fullgo test ./gateway/...green (incl. connectsrv abuse + the new guard).vitest run619 passed / 9 skipped;svelte-check0 errors / 0 warnings.