#286: request() now throws C2ApiError — message is FastAPI's `detail` or a plain sentence for the status (401/403/404/429/5xx), with toString() so the many String(e) call sites read cleanly. status/body stay on the error; the Discord parsers rebuild the legacy "C2 API error N: body" text via legacyErrorText(), so their invite/code parsing is unchanged. #284: Resolve, Regenerate summary, alert-rule toggle/delete/load, Acknowledge, trip deletes, system delete and Restart OP25 now surface failures (ErrorBanner where the page has one, alert() where the page already uses it). Rules tab no longer says "No alert rules configured" when the load failed. Confirms added for Resolve (no reopen path exists), rule delete and trip delete. Reopen deliberately not built — manual resolve doesn't set resolved_at, so reopening is a lifecycle change. Frontend unverified: no Node on the authoring box (#207). Closes #286 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
65 lines
2.3 KiB
TypeScript
65 lines
2.3 KiB
TypeScript
/**
|
|
* Error thrown by every C2 API request (server-26#286).
|
|
*
|
|
* `message` is a sentence a user can read — FastAPI's `detail` when the
|
|
* server sent one, otherwise a plain mapping of the status. The raw status
|
|
* and body stay on the error for callers that need the structured parts
|
|
* (the Discord parsers read `invite`/`code` out of `body`).
|
|
*/
|
|
export class C2ApiError extends Error {
|
|
readonly status: number;
|
|
readonly body: string;
|
|
|
|
constructor(status: number, body: string) {
|
|
super(humanMessage(status, body));
|
|
this.name = "C2ApiError";
|
|
this.status = status;
|
|
this.body = body;
|
|
}
|
|
|
|
// Call sites render String(e); without this it reads "C2ApiError: …".
|
|
toString(): string {
|
|
return this.message;
|
|
}
|
|
}
|
|
|
|
function detailOf(body: string): string | null {
|
|
try {
|
|
const parsed = JSON.parse(body);
|
|
const d = parsed?.detail;
|
|
if (typeof d === "string" && d.trim()) return d;
|
|
// pydantic validation errors: [{loc, msg, type}, ...]
|
|
if (Array.isArray(d)) {
|
|
const msgs = d.map((x: { msg?: unknown }) => (typeof x?.msg === "string" ? x.msg : null)).filter(Boolean);
|
|
if (msgs.length) return msgs.join("; ");
|
|
}
|
|
} catch {
|
|
/* not JSON */
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function humanMessage(status: number, body: string): string {
|
|
const detail = detailOf(body);
|
|
if (status === 401) return "Your session has expired. Sign in again.";
|
|
if (status === 403) return detail ? `Not allowed: ${detail}` : "You don't have permission to do that.";
|
|
if (detail) return detail;
|
|
if (status === 404) return "Not found. It may have been deleted.";
|
|
if (status === 429) return "Too many requests. Wait a moment and try again.";
|
|
if (status >= 500) return "The server hit an error. Try again in a moment.";
|
|
return `Request failed (${status}).`;
|
|
}
|
|
|
|
/** Message for any caught value — use in catch blocks instead of String(e). */
|
|
export function errorMessage(e: unknown): string {
|
|
if (e instanceof Error) return e.message;
|
|
return String(e);
|
|
}
|
|
|
|
/** Rebuilds the legacy `C2 API error <status>: <body>` string for the
|
|
* Discord parsers, which predate C2ApiError and match on that shape. */
|
|
export function legacyErrorText(err: unknown): string {
|
|
if (err instanceof C2ApiError) return `C2 API error ${err.status}: ${err.body}`;
|
|
return err instanceof Error ? err.message : String(err);
|
|
}
|