fix(errors): the error banner says what failed and no longer stacks #210

Merged
james.maes merged 2 commits from fix/error-banner-names-cause into main 2026-11-11 00:58:18 +00:00
Owner

What

The global query/mutation error banner (handleQueryError in src/lib/query-client.ts) showed
"Something went wrong" for every failure without an expected status, and a polling query that kept
failing added one identical banner per attempt, so they stacked at the top of the window.

Banner text

Failure Text Line below it
No response (connection failed, reset) No response from the server POST /qqq/v1/table/order/query
No response (the 30 s timeout) The server took too long to respond the request
A failed fetch (no axios request) No response from the server While loading records (query key namespace, in plain words)
Unexpected status (400, 409, 422, ...) the server's message (userFacingError, error, message of a JSON body), else Request failed (HTTP 409) the request, plus (HTTP 409) when the text does not already say it
403 the server's message, else Permission denied (was the axios transport text when the body was not JSON) the request (HTTP 403)
404 / 5xx unchanged: Resource not found, Server error — retrying…, Server error — please try again the request (HTTP nnn)
An error the application raised that sentence While loading <area>
A runtime error (TypeError, a schema error) The page hit an unexpected error (TypeError) While loading <area>
401, axios cancel, aborted fetch, meta.handlesOwnErrors none

The request line is METHOD /path: no query string, and every path segment that is not a plain word
(a record id, a UUID, an encoded value) is shown as {id}. No body, header, token or query string
reaches a banner or the console.

No stacking

The banner's sonner id is its text plus the request line, so a repeat of the same failure updates
the banner on screen and a different failure gets its own. A lost connection is one cause whichever
request met it, so it has a single banner, which names the request that failed most recently.

Console

Each banner logs once per failure: console.warn('[API] Request failed:', { context, status, code, name, request, area }). Never a message, body, header or query string.

Tests

src/lib/query-client.test.ts covers: no status (axios network error, timeout, failed fetch), an
unexpected 4xx with and without a server message, 403/404/5xx, path masking, a repeat producing one
banner on a real Toaster (and a different failure a second one), one banner for a lost connection,
silence for a cancelled request, an aborted fetch, 401 and handlesOwnErrors, and the console line.

Gates

Run locally, one at a time, as .gitea/workflows/build.yaml runs them. Results are in the comment
below. Playwright e2e was not run locally; it is left to CI.

Draft: do not merge, tag or publish.

🤖 Generated with Claude Code

## What The global query/mutation error banner (`handleQueryError` in `src/lib/query-client.ts`) showed "Something went wrong" for every failure without an expected status, and a polling query that kept failing added one identical banner per attempt, so they stacked at the top of the window. ## Banner text | Failure | Text | Line below it | |---|---|---| | No response (connection failed, reset) | `No response from the server` | `POST /qqq/v1/table/order/query` | | No response (the 30 s timeout) | `The server took too long to respond` | the request | | A failed `fetch` (no axios request) | `No response from the server` | `While loading records` (query key namespace, in plain words) | | Unexpected status (400, 409, 422, ...) | the server's message (`userFacingError`, `error`, `message` of a JSON body), else `Request failed (HTTP 409)` | the request, plus `(HTTP 409)` when the text does not already say it | | 403 | the server's message, else `Permission denied` (was the axios transport text when the body was not JSON) | the request `(HTTP 403)` | | 404 / 5xx | unchanged: `Resource not found`, `Server error — retrying…`, `Server error — please try again` | the request `(HTTP nnn)` | | An error the application raised | that sentence | `While loading <area>` | | A runtime error (`TypeError`, a schema error) | `The page hit an unexpected error (TypeError)` | `While loading <area>` | | 401, axios cancel, aborted `fetch`, `meta.handlesOwnErrors` | none | | The request line is `METHOD /path`: no query string, and every path segment that is not a plain word (a record id, a UUID, an encoded value) is shown as `{id}`. No body, header, token or query string reaches a banner or the console. ## No stacking The banner's sonner `id` is its text plus the request line, so a repeat of the same failure updates the banner on screen and a different failure gets its own. A lost connection is one cause whichever request met it, so it has a single banner, which names the request that failed most recently. ## Console Each banner logs once per failure: `console.warn('[API] Request failed:', { context, status, code, name, request, area })`. Never a message, body, header or query string. ## Tests `src/lib/query-client.test.ts` covers: no status (axios network error, timeout, failed fetch), an unexpected 4xx with and without a server message, 403/404/5xx, path masking, a repeat producing one banner on a real `Toaster` (and a different failure a second one), one banner for a lost connection, silence for a cancelled request, an aborted fetch, 401 and `handlesOwnErrors`, and the console line. ## Gates Run locally, one at a time, as `.gitea/workflows/build.yaml` runs them. Results are in the comment below. Playwright e2e was not run locally; it is left to CI. Draft: do not merge, tag or publish. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
james.maes added 1 commit 2026-11-10 17:30:22 +00:00
fix(errors): the error banner says what failed and no longer stacks
maven / build (push) Successful in 4m33s
build / maven (push) Successful in 4m33s
523f59cf38
The global query/mutation error handler showed "Something went wrong" for
every failure without an expected status, and a polling query that kept
failing added one banner per attempt.

- No response: "No response from the server" (or "The server took too long
  to respond" for a timeout), with the request below it as METHOD /path:
  no query string, and ids shown as {id}.
- Unexpected status: the server's own message, else "Request failed (HTTP 409)".
- An error raised by the application shows its own sentence; a runtime error
  shows only its type.
- The banner's sonner id is its text plus the request, so a repeat updates
  the banner on screen. A lost connection has one banner for every request.
- 401, cancelled requests and aborted fetches stay silent. 403 falls back to
  "Permission denied" instead of the axios transport text.
- Each banner logs status, axios code, error name and request to the console,
  never a message, body, header or query string.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
james.maes added 1 commit 2026-11-10 17:39:03 +00:00
docs(releases): note the 403 fallback and the silent cases of the error banner
maven / build (push) Successful in 4m13s
build / maven (push) Successful in 4m13s
4347924658
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Author
Owner

Gate results, run locally one command at a time on Windows (Node 24.22.0, pnpm 9.15.9) at 523f59cf. The head 43479246 adds one sentence to docs/releases/unreleased.md only.

Gate Result
pnpm install --frozen-lockfile lockfile up to date, nothing to install
pnpm typecheck pass (exit 0)
pnpm lint pass: 0 errors, 6 warnings, all in files this PR does not touch; license headers pass
pnpm test pass: 364 files, 3835 tests passed, 1 skipped
pnpm test:coverage pass, thresholds met: statements 93.49 %, branches 87.34 %, functions 94.59 %, lines 95.33 %
pnpm build:export pass (exit 0)
Gitea build / maven success on 523f59cf (4 m 33 s) and on 43479246

Not run: Playwright e2e and acceptance suites, Storybook build, bundle budget (they run in the GitHub workflows, and this branch is on Gitea only), and no browser check of the banner in the running app. The description line under the banner text is the first use of a sonner description in this app; its look in light and dark mode has not been seen in a browser.

Gate results, run locally one command at a time on Windows (Node 24.22.0, pnpm 9.15.9) at `523f59cf`. The head `43479246` adds one sentence to `docs/releases/unreleased.md` only. | Gate | Result | |---|---| | `pnpm install --frozen-lockfile` | lockfile up to date, nothing to install | | `pnpm typecheck` | pass (exit 0) | | `pnpm lint` | pass: 0 errors, 6 warnings, all in files this PR does not touch; license headers pass | | `pnpm test` | pass: 364 files, 3835 tests passed, 1 skipped | | `pnpm test:coverage` | pass, thresholds met: statements 93.49 %, branches 87.34 %, functions 94.59 %, lines 95.33 % | | `pnpm build:export` | pass (exit 0) | | Gitea `build / maven` | success on `523f59cf` (4 m 33 s) and on `43479246` | Not run: Playwright e2e and acceptance suites, Storybook build, bundle budget (they run in the GitHub workflows, and this branch is on Gitea only), and no browser check of the banner in the running app. The description line under the banner text is the first use of a sonner `description` in this app; its look in light and dark mode has not been seen in a browser.
james.maes marked the pull request as ready for review 2026-11-11 00:58:16 +00:00
james.maes merged commit 24bbe4a038 into main 2026-11-11 00:58:18 +00:00
Sign in to join this conversation.