can1357/oh-my-pi · error · ApiError

extractDetail(body) ?? resp.statusText ?? `HTTP ${resp.statu

Error message

extractDetail(body) ?? resp.statusText ?? `HTTP ${resp.status}`

What it means

The dashboard API client's unwrap() throws ApiError whenever a fetch resolves with a non-ok HTTP response. It tries resp.json(), extracts a 'detail' or 'message' string from the parsed body (FastAPI's error shape), and falls back to resp.statusText then 'HTTP {status}'. Any backend rejection — 400/401/404/409 from endpoints like /api/cancel or /api/trigger — surfaces here as a thrown ApiError with the status attached.

Source

Thrown at python/robomp/web/src/api.ts:38

  if (body == null || typeof body !== "object") return null;
  const detail = (body as Record<string, unknown>).detail;
  if (typeof detail === "string") return detail;
  const message = (body as Record<string, unknown>).message;
  if (typeof message === "string") return message;
  return null;
}

async function unwrap<T>(resp: Response): Promise<T> {
  let body: unknown = null;
  try {
    body = await resp.json();
  } catch {
    // Endpoint returned non-JSON. For 2xx that's still valid for callers that
    // expect an empty body; we only surface the parse failure on errors.
  }
  if (!resp.ok) {
    const detail = extractDetail(body) ?? resp.statusText ?? `HTTP ${resp.status}`;
    throw new ApiError(resp.status, detail);
  }
  return body as T;
}

function authHeaders(): Record<string, string> {
  return { ...AUTH_HEADERS };
}

function jsonHeaders(): Record<string, string> {
  return { "Content-Type": "application/json", ...AUTH_HEADERS };
}

export const api = {
  status(signal?: AbortSignal): Promise<StatusResponse> {
    return fetch("/api/status", { signal }).then(unwrap<StatusResponse>);
  },
  logs(limit = 400, signal?: AbortSignal): Promise<LogsResponse> {
    return fetch(`/api/logs?limit=${limit}`, { signal }).then(unwrap<LogsResponse>);

View on GitHub (pinned to 9690622007)

Solutions

  1. Read err.status and err.message (the detail field) — the FastAPI backend always includes a descriptive detail for 4xx from these endpoints.
  2. Fix the triggering request: for cancel errors, refresh state and only cancel deliveries shown as running; for trigger errors, supply a valid issue or delivery_id.
  3. Check AUTH_HEADERS / X-Robomp-Replay-Token configuration in web/src/config if 401/403.
  4. If the message is generic statusText, the server returned non-JSON — check backend logs and the Vite proxy target (:8080).
  5. Handle the promise rejection in the UI (toast/error state) instead of letting it bubble as an unhandled rejection.

Example fix

// before
api.cancel(id); // unhandled ApiError
// after
try {
  await api.cancel(id);
} catch (e) {
  if (e instanceof ApiError && e.status === 409) showToast("Task already finished");
  else showToast(`Cancel failed: ${e instanceof ApiError ? e.message : e}`);
}
Defensive patterns

Strategy: try-catch

Validate before calling

function assertNonEmpty(v: string | undefined, name: string): string {
  if (!v) throw new Error(`${name} is required before calling the API`);
  return v;
}
assertNonEmpty(deliveryId, 'delivery_id');

Type guard

function isApiError(e: unknown): e is ApiError {
  return e instanceof ApiError && typeof e.status === 'number';
}

Try / catch

try {
  await api.cancel(id);
} catch (e) {
  if (isApiError(e)) {
    showError(e.status === 409 ? 'Already finished' : e.message);
  } else {
    showError('Network failure'); // fetch threw before unwrap
  }
}

Prevention

When it happens

Trigger: Any api.* call whose response has status >= 400: cancel of unknown/finished delivery (404/409), trigger with missing/invalid payload fields, missing or wrong X-Robomp-Replay-Token auth header (401/403), server returning HTML error pages (non-JSON body → detail falls back to statusText), network-level proxy errors.

Common situations: AUTH_HEADERS misconfigured or token rotated so requests are rejected; dashboard stale state → cancelling an already-finished delivery; backend down behind the Vite proxy (proxy returns 500/502 with non-JSON body); bug sending undefined delivery_id → 400.

Understand the failure class

Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/6fe6587d0abbe8ff. Report an issue: GitHub.