Skip to content

fix(api): stop reporting failed writes as HTTP 200 - #291

Open
HugoFara wants to merge 1 commit into
developfrom
fix/284-error-payloads-return-2xx
Open

fix(api): stop reporting failed writes as HTTP 200#291
HugoFara wants to merge 1 commit into
developfrom
fix/284-error-payloads-return-2xx

Conversation

@HugoFara

Copy link
Copy Markdown
Owner

Fixes #284.

The defect

Handlers across the API report failure by returning ['error' => ...] or ['success' => false, ...] rather than by throwing, and every router hands that return value straight to Response::success(). Sent as-is it became HTTP 200fetch reported ok, the client took the payload as data, and the interface silently did nothing.

This is what made #283 invisible: POST /api/v1/terms/quick answered 200, changed no rows, logged no error, and updated nothing on screen.

The fix

Two halves.

Backend — the shape is recognised at the single chokepoint Response::success() and given a 400, rather than rewriting the ~300 return sites. The body is passed through untouched, so frontend code already reading the message out of the payload keeps working; only the status becomes honest.

Detection is deliberately narrow: 'error' => null is what handlers emit on the way out of a success branch, so presence of the key means nothing — the value has to be a non-empty string (or a bare true flag). 'errors' => [...], a result field on batch endpoints, is not a failure signal. A caller that named its own status keeps it.

Frontend — the API client only ever looked for message on a failed request, while the API sends error. Even a correctly-formed Response::error() therefore surfaced as a bare HTTP 400: Bad Request, with the real reason discarded. All six wrappers now share one extractor that reads both, plus the status line as a last resort. apiPostMultipart already did this by hand; that copy is now the shared one.

Why no call-site churn

I checked all 75 places the frontend reads an error out of an ApiResponse body. Every one of them already checks response.error first (or falls back to it), so they now receive the more specific message rather than losing it. The response.data.error branches become unreachable for promoted responses but are kept — endpoints that bypass Response::success() (DictionaryController echoes JSON directly) still return errors inside a 200.

Note for API consumers

This is a visible API change: endpoints that answered 200-with-an-error now answer 400. That is the point of the issue, but it is worth knowing for the Lukaisu client and any external consumer.

Verification

  • BookApiHandlerTest::routePostHandlesTheCollection asserted 200 for what its own comment called "the handled-failure path" — it was documenting the defect. Updated to 400.
  • PHPUnit 9137 pass, 0 failures
  • Vitest 4335 pass (131 files), including 5 new client tests
  • Psalm 0 errors, PHPCS clean, tsc --noEmit clean, ESLint clean

Handlers across the API report a failure by returning `['error' => ...]`
or `['success' => false, ...]` rather than by throwing, and every router
hands that return value straight to `Response::success()`. Sent as-is it
became HTTP 200: `fetch` reported `ok`, the client took the payload as
data, and the interface silently did nothing.

Recognise the shape at that one chokepoint and give it a 400 instead of
rewriting 300-odd return sites. The body is passed through untouched, so
the frontend code that reads the message out of the payload keeps
working — only the status becomes honest. `'error' => null` is what
handlers emit on the way out of a *success* branch, so the value has to
be a non-empty message (or a bare `true` flag) to count.

The API client was the other half. It only ever looked for `message` on
a failed request, while the API sends `error`, so even a correctly-formed
error response surfaced as a bare "HTTP 400: Bad Request". All six
wrappers now share one extractor that reads both.

BookApiHandlerTest asserted 200 for what its own comment called "the
handled-failure path" — it was documenting the defect, and now expects
400.

Fixes #284
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant