Files
meshcore-analyzer/tests/unit
9534e1c25e fix(reach): retry a busy (429) reach report in the reach page (#2250)
Follow-up to #2127: @dborup's point 2 there, and @efiten's non-blocking
note on the approval ("worth checking the node page's reach panel shows
a retry hint on 429 rather than a generic error").

## What changes

- **The reach page retries a 429 itself.** It shows "Reach is busy
computing other reports, retrying in N s", waits `Retry-After` seconds,
and tries again.
- When the header is missing or is not a number of seconds (an
HTTP-date, 0, a negative number), it waits 2 s. It never waits more than
30 s.
- After 5 retries it shows the old error ("Failed to load reach: API 429
…").
- Leaving the page stops the retries, both while it waits and while a
request is still in flight. Picking another window while a request is in
flight also drops that request's retries.
- **`api()` does not retry 429.** Login, signup, activation, email
change, companion linking, settings PUT and channel proposals also
answer 429 from their own limits, and those must not auto-retry. `api()`
now puts `status` and `retryAfter` on the errors it throws, so a caller
can decide; the message text is unchanged. A unit test pins that `api()`
makes exactly one request on a 429.
- **Fix in `api()`:** the `_inflight` cleanup chain
(`promise.finally(...)`) rejected unhandled on every failed call, so
each API error also logged "Uncaught (in promise)". That would have
added up to 5 more per busy reach load. The chain now catches; callers
still get the rejection through the returned promise.

This works with the server as it is on master (instant 429,
`Retry-After: 5`), and with the bounded wait in #2249 (429 after 3 s,
`Retry-After: 2`). Neither PR depends on the other.

## What I checked

- **Suites.** `sh test-all.sh` passes, the XSS sink gate passes, and
eslint has 0 errors and no new warnings.
- **Unit tests** (`test-frontend-helpers.js`):
- `reachRetryDelayMs`: Retry-After values; fallback on missing, zero,
negative or HTTP-date values; the 30 s cap; giving up after 5 retries;
no retry on 400, 404, 500, 503 or network errors.
- `api()`: a 429 error carries `status` and `retryAfter`, makes exactly
one request, and leaves no unhandled rejection.
- The block sits above the file's `Promise.allSettled(pendingTests)`
summary. Async tests registered below that line are never awaited, so
they cannot fail the run; nothing below it is async today.
- **E2E** (`test-node-reach-e2e.js`, already in CI suite 2): reach
requests are answered through `page.route`, so the server does not have
to be made busy. It checks:
- two 429s, then the real report: the hint shows "retrying in 1 s", the
retries wait (at least 1.8 s between the first and third request), and
no error is shown;
  - always 429: 6 requests (1 + 5 retries), then the error;
  - leaving while it waits: no more requests;
- leaving while the first request is still in flight: no hint written,
no retry;
  - a 500 with Retry-After: one request, no retry.

On master's frontend the new section fails at the first busy hint.
Locally against the freshened `e2e-fixture.db`, the whole file takes
about 17 s (2 runs).
- **Mutations.** I wrote 15 from the code, and the tests caught all 15.
They include: `api()` drops `status` or `retryAfter`, `api()` retries
429 itself, retry any error, retry limit off by one, fallback or cap
changed, Retry-After 0 accepted, either "left the page" check removed,
the wait not honoured, the hint showing ms, the retry count ignored, no
error after giving up, and the cleanup chain unhandled again. Each
mutation was checked against the file the server actually served before
the tests ran.
- **Real browser** (Chromium via Playwright, on a copy of a
10.5M-observation database):
- Current master's server, 6 tabs opened at once on uncached 14-day
reports: 4 got 429 with `Retry-After: 5` and showed the hint, and all 6
reports rendered within 11.5 s (one tab retried twice). Without this
change those 4 tabs show "Failed to load reach: API 429".
- With #2249's server: 4 of 6 got 429 with `Retry-After: 2`, and all
rendered within 8.1 s.
- The only console errors were Chrome's own "Failed to load resource"
lines for the 429s; there was no "Uncaught (in promise)".

## Not checked

- **Mobile layout.** The hint reuses the existing `.nq-load` style, so I
did not take a screenshot at phone width.
- **Other browsers.** Firefox and Safari were not tried; the code is
plain `setTimeout` / `await`.
- **Window switches.** While the hint is showing, the time-range buttons
are not on screen; the same is true of the existing error and loading
states. Picking a new window means waiting, or going back and opening
reach again.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: nullrouten0 <201262769+nullrouten0@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 11:58:24 +02:00
..