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
- Read err.status and err.message (the detail field) — the FastAPI backend always includes a descriptive detail for 4xx from these endpoints.
- 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.
- Check AUTH_HEADERS / X-Robomp-Replay-Token configuration in web/src/config if 401/403.
- If the message is generic statusText, the server returned non-JSON — check backend logs and the Vite proxy target (:8080).
- 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
- Always await api.* calls inside try/catch — unwrap throws ApiError for every non-2xx.
- Validate payload fields (issue, delivery_id) before calling trigger/cancel.
- Keep AUTH_HEADERS (X-Robomp-Replay-Token) in sync with the backend token.
- Match FastAPI's error shape ({detail}) when adding new endpoints so extractDetail keeps working.
- Check the Vite proxy target (:8080) and backend health when errors carry only statusText.
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
- AnthropicConnectionError
- AnthropicConnectionTimeoutError
- Devin API error ${response.status} ${response.statusText}: $
- Failed to initiate device authorization: ${initiateResponse.
- Kimi device flow failed: ${error ?? response.status}${descri
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/6fe6587d0abbe8ff.
Report an issue: GitHub.