FastAPI validation errors (HTTP 422) are hidden from users — readErrorBody ignores body.detail #118

Closed
opened 2026-09-30 01:48:43 +00:00 by Mark · 1 comment
Owner

Bug: 422 errors from easystitch-service show "easystitch-service responded 422" with no actionable detail

Reproduction (verified 2026-09-25)

Trigger any 422 from /v1/digitize. Easiest way: pick detail stitch preference (see #116) and click Run.

Service response body:

{"detail":"invalid settings JSON: 1 validation error for DigitizeSettings\nstitchPreference\n  Input should be 'balanced', 'quality' or 'speed' [type=literal_error, input_value='detail', input_type=str]"}

Expected

User sees a toast like "Invalid stitch preference: expected one of 'balanced', 'quality', 'speed'".

Actual

User sees: "easystitch-service responded 422".

Root cause

src/lib/easystitch-client.ts:381-407 readErrorBody():

async function readErrorBody(response: Response): Promise<{code: string; message: string}> {
  const fallback = {
    code: "unknown_error",
    message: `easystitch-service responded ${response.status}`,
  };
  const contentType = response.headers.get("content-type") ?? "";
  if (!contentType.includes("application/json")) {
    return fallback;
  }
  try {
    const body = (await response.json()) as Record<string, unknown>;
    const code =
      typeof body.error === "string" ? body.error : fallback.code;
    const message =
      typeof body.message === "string"
        ? body.message
        : typeof body.error === "string"
          ? body.error
          : fallback.message;
    return { code, message };
  } catch {
    return fallback;
  }
}

Looks at body.error and body.message. FastAPI uses body.detail (singular array of validation errors or a string). Mismatch → fallback.

Change readErrorBody() to also check body.detail:

const detail =
  typeof body.detail === "string"
    ? body.detail
    : Array.isArray(body.detail) && body.detail.length > 0 && typeof body.detail[0]?.msg === "string"
      ? body.detail.map((d: any) => d.msg ?? JSON.stringify(d)).join("; ")
      : null;

const message =
  typeof body.message === "string" ? body.message :
  typeof body.error === "string" ? body.error :
  detail ?? fallback.message;

Priority order: body.message → body.error → body.detail (string or joined msg array) → fallback.

Add a unit test for readErrorBody():

  • 422 with {"detail":[{"msg":"Invalid stitchPreference"}]} → message = "Invalid stitchPreference"
  • 422 with {"detail":"some string"} → message = "some string"
  • 500 with {"error":"server error","message":"boom"} → message = "boom"
  • non-JSON 500 → message = "easystitch-service responded 500"

Severity

Medium. Users hit generic error toasts with no actionable info. Hides #116 entirely (the user just sees "422" without knowing their stitch preference is the cause).

Labels

  • bug
  • triage-approved
  • front-end

Filed 2026-09-25 by user-driven bughunt session. Supersedes umbrella #115.

## Bug: 422 errors from easystitch-service show "easystitch-service responded 422" with no actionable detail ### Reproduction (verified 2026-09-25) Trigger any 422 from `/v1/digitize`. Easiest way: pick `detail` stitch preference (see #116) and click Run. Service response body: ```json {"detail":"invalid settings JSON: 1 validation error for DigitizeSettings\nstitchPreference\n Input should be 'balanced', 'quality' or 'speed' [type=literal_error, input_value='detail', input_type=str]"} ``` ### Expected User sees a toast like "Invalid stitch preference: expected one of 'balanced', 'quality', 'speed'". ### Actual User sees: "easystitch-service responded 422". ### Root cause `src/lib/easystitch-client.ts:381-407` `readErrorBody()`: ```ts async function readErrorBody(response: Response): Promise<{code: string; message: string}> { const fallback = { code: "unknown_error", message: `easystitch-service responded ${response.status}`, }; const contentType = response.headers.get("content-type") ?? ""; if (!contentType.includes("application/json")) { return fallback; } try { const body = (await response.json()) as Record<string, unknown>; const code = typeof body.error === "string" ? body.error : fallback.code; const message = typeof body.message === "string" ? body.message : typeof body.error === "string" ? body.error : fallback.message; return { code, message }; } catch { return fallback; } } ``` Looks at `body.error` and `body.message`. FastAPI uses `body.detail` (singular array of validation errors or a string). Mismatch → fallback. ### Recommended fix Change `readErrorBody()` to also check `body.detail`: ```ts const detail = typeof body.detail === "string" ? body.detail : Array.isArray(body.detail) && body.detail.length > 0 && typeof body.detail[0]?.msg === "string" ? body.detail.map((d: any) => d.msg ?? JSON.stringify(d)).join("; ") : null; const message = typeof body.message === "string" ? body.message : typeof body.error === "string" ? body.error : detail ?? fallback.message; ``` Priority order: `body.message` → `body.error` → `body.detail` (string or joined `msg` array) → fallback. Add a unit test for `readErrorBody()`: - 422 with `{"detail":[{"msg":"Invalid stitchPreference"}]}` → message = "Invalid stitchPreference" - 422 with `{"detail":"some string"}` → message = "some string" - 500 with `{"error":"server error","message":"boom"}` → message = "boom" - non-JSON 500 → message = "easystitch-service responded 500" ### Severity **Medium.** Users hit generic error toasts with no actionable info. Hides #116 entirely (the user just sees "422" without knowing their stitch preference is the cause). ### Labels - bug - triage-approved - front-end Filed 2026-09-25 by user-driven bughunt session. Supersedes umbrella #115.
Author
Owner

Already shipped in commit 2b2f630 (fix(#115): auto-digitize settings wiring — stitch pref translation, underlay wire field, 422 detail surfaced), on origin/main of Mark/Kimi_Agent_Brother_10_Digitizing_App. This commit extended the umbrella #115 fix to cover exactly the readErrorBody() bug filed in #118: pickDetailMessage() extracts body.detail (string OR FastAPI 422 validation-error array) and the message priority is now detail -> message -> error -> fallback, so users see the validation error instead of "easystitch-service responded 422".

Tests covering all four cases in src/lib/easystitch-client.test.ts::describe(readErrorBody):

  • 422 with {detail: [...]} (RequestValidationError array) -> message contains stitchPreference / quality
  • 422 with {detail: "<long string>"} (HTTPException) -> message contains the detail text
  • 500 with {message: "queue is full"} -> falls back to body.message
  • 500 with {error: "internal"} only -> falls back to body.error
  • non-JSON 502 -> easystitch-service responded 502

Vitest: 51/51 pass on src/lib/easystitch-client.test.ts (run 2026-09-29). Closing #118 as effectively a duplicate of #115 -- same commit, same code change.

(Closing this card via triage-runner; no further work pending.)

Already shipped in commit 2b2f630 (`fix(#115): auto-digitize settings wiring — stitch pref translation, underlay wire field, 422 detail surfaced`), on `origin/main` of Mark/Kimi_Agent_Brother_10_Digitizing_App. This commit extended the umbrella #115 fix to cover exactly the `readErrorBody()` bug filed in #118: `pickDetailMessage()` extracts `body.detail` (string OR FastAPI 422 validation-error array) and the message priority is now `detail -> message -> error -> fallback`, so users see the validation error instead of "easystitch-service responded 422". Tests covering all four cases in `src/lib/easystitch-client.test.ts::describe(readErrorBody)`: - 422 with `{detail: [...]}` (RequestValidationError array) -> message contains stitchPreference / quality - 422 with `{detail: "<long string>"}` (HTTPException) -> message contains the detail text - 500 with `{message: "queue is full"}` -> falls back to body.message - 500 with `{error: "internal"}` only -> falls back to body.error - non-JSON 502 -> `easystitch-service responded 502` Vitest: 51/51 pass on `src/lib/easystitch-client.test.ts` (run 2026-09-29). Closing #118 as effectively a duplicate of #115 -- same commit, same code change. (Closing this card via triage-runner; no further work pending.)
Mark closed this issue 2026-09-30 02:07:28 +00:00
Sign in to join this conversation.
No description provided.