/** * Turning a thrown request error into a sentence a person can act on. * * ── The one this exists for ── * * `useClient` throws `ApiError(status, message)` where `message` is the response body as text. That is * right when the body came from Officer. It is badly wrong when the body came from something BETWEEN the * browser and Officer — because then the message is a whole HTML error page, and the panel renders it * verbatim, tags and all. * * Seen on 2026-08-25, on this plugin's first real test: installing it restarts `officer`, and a request * made during that window reached Nginx Proxy Manager, found nothing behind it, and came back as * openresty's `502 Bad Gateway` page. The panel printed the markup. * * The irony is worth keeping: this plugin manages the proxy that produced the page it could not read. * * ── Why the check is "does it look like HTML" ── * * There is no status that separates the two. A 502 from the proxy and a 502 from this plugin's own router * are the same number — `api/router.ts` uses 502 for "NPM answered and refused", which is a real, useful * answer we must not swallow. What differs is the SHAPE of the body: ours is JSON and unwrapped to a * sentence by the client, theirs is a document. So the body is the signal, and it is a cheap one. */ /** A body that is a document rather than a message. Cheap and deliberately loose. */ const looksLikeHtml = (text: string): boolean => /^\s*<(!doctype|html|head|body)/i.test(text); /** * A gateway status with an HTML body means the hop in front of Officer answered, not Officer. * * Both halves are required. A gateway status alone is ambiguous — this plugin's own router returns 502 * and 503 with real meaning — and HTML alone could be anything. */ const isProxyError = (status: number | undefined, message: string): boolean => (status === 502 || status === 503 || status === 504) && looksLikeHtml(message); export const readableError = (error: unknown, fallback = 'Something went wrong.'): string => { const status = (error as { status?: number })?.status; const message = error instanceof Error ? error.message : ''; if (isProxyError(status, message)) { return 'Officer itself did not answer — it is probably restarting. Wait a moment and try again.'; } // A document from anywhere else is still not a sentence. Say what happened rather than printing markup. if (looksLikeHtml(message)) return status ? `The server answered ${status} with a web page, not an answer.` : fallback; return message || fallback; };