do not print a proxy's error page at somebody
first real test of the settings form returned openresty's 502 Bad Gateway document, markup and all, into the panel. installing this plugin restarts officer, and a request made inside that window reaches nginx proxy manager, finds nothing behind it, and gets a web page back. useClient throws the response body as the message, so the panel printed it. worth keeping the irony: this plugin manages the proxy whose error page it could not read. the check is the body's SHAPE, not the status, because there is no status that separates them — api/router.ts uses 502 for "NPM answered and refused", which is a real answer we must not swallow. ours arrives as a sentence, theirs as a document. gateway status AND html means the hop in front of officer answered; html on its own still is not a sentence and says so without the tags. all three panels go through it now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
Reference in New Issue
Block a user