fix(admin): stop reporting every removeImageBackground failure as a sidecar failure (#281)
The remove-background route's catch block turned every throw from removeImageBackground into a 502 "the background-removal service did not answer". But removeImageBackground also throws for an unrecognised file extension (a legacy .jpeg), for a file missing from the uploads volume, and when REMBG_URL is not set at all — none of which involve contacting the sidecar. The admin was told to retry a service that was never reached, while the real reason existed only in the server log. Added SidecarRequestError in rembgClient.ts, following the NoOriginalToRestoreError pattern already in backgroundRemoval.ts. It is thrown only for failures that happen after actually attempting to reach the sidecar: the fetch call itself throwing (now wrapped in a try/catch, covering unreachable and timed-out), a non-2xx response, or a response that is not a PNG. It is deliberately not thrown for "REMBG_URL is not set", since that path never attempts contact at all. The remove-background handler now checks err instanceof SidecarRequestError before answering 502; everything else answers 500 with a message that says what actually went wrong. Added a unit test pairing (rembgClient.test.ts) asserting the sidecar-contacted failures are SidecarRequestError and the unconfigured case is not, and an integration test (adminItemDrafts.integration.test.ts) proving a missing upload file answers something other than 502 with a message that does not claim the service did not answer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,18 @@ const MODEL = 'u2net';
|
||||
*/
|
||||
const TIMEOUT_MS = 120_000;
|
||||
|
||||
/**
|
||||
* Thrown only when the sidecar was actually contacted and did not answer
|
||||
* usably — unreachable, timed out, answered with a non-2xx status, or
|
||||
* answered with something that is not a PNG.
|
||||
*
|
||||
* Deliberately not thrown for "REMBG_URL is not set": that failure happens
|
||||
* before any attempt to contact anything, so lumping it in here would tell a
|
||||
* caller "the service did not answer" about a service nothing ever tried to
|
||||
* reach. A caller distinguishes the two to avoid exactly that (#281 review).
|
||||
*/
|
||||
export class SidecarRequestError extends Error {}
|
||||
|
||||
/** The configured base URL, or null when there is none. */
|
||||
function baseUrl(): string | null {
|
||||
const raw = process.env.REMBG_URL;
|
||||
@@ -72,14 +84,24 @@ export async function removeBackground(bytes: Buffer, mediaType: string): Promis
|
||||
body.append('file', new Blob([new Uint8Array(bytes)], { type: mediaType }), 'photo');
|
||||
body.append('model', MODEL);
|
||||
|
||||
const res = await fetch(`${base}/api/remove`, {
|
||||
method: 'POST',
|
||||
body,
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS)
|
||||
});
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(`${base}/api/remove`, {
|
||||
method: 'POST',
|
||||
body,
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS)
|
||||
});
|
||||
} catch (err) {
|
||||
// Unreachable, refused, or timed out — fetch throws for all three rather
|
||||
// than returning a response, so this is the only place that can catch
|
||||
// them and mark them as a sidecar failure rather than a generic error.
|
||||
throw new SidecarRequestError(
|
||||
`rembg did not answer: ${err instanceof Error ? err.message : String(err)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`rembg answered ${res.status}`);
|
||||
throw new SidecarRequestError(`rembg answered ${res.status}`);
|
||||
}
|
||||
|
||||
const out = Buffer.from(await res.arrayBuffer());
|
||||
@@ -88,7 +110,7 @@ export async function removeBackground(bytes: Buffer, mediaType: string): Promis
|
||||
// image/png would otherwise be written over a photograph — the same reason
|
||||
// uploads are checked by signature rather than by what the caller declared.
|
||||
if (!signatureMatches('image/png', out.subarray(0, SIGNATURE_BYTES))) {
|
||||
throw new Error('rembg response is not a PNG');
|
||||
throw new SidecarRequestError('rembg response is not a PNG');
|
||||
}
|
||||
|
||||
return out;
|
||||
|
||||
Reference in New Issue
Block a user