1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
import { warn } from "$lib/logging";
// Decode + resize an image in a throwaway child process (lib/image-worker.ts)
// so a native decoder crash cannot take the app down. Resolves with the webp
// bytes; rejects with a short message on any failure (bad image, crash,
// timeout). Never import sharp in the main process — that is the whole point.
const WORKER = process.env.IMAGE_WORKER || "/home/bun/app/lib/image-worker.ts";
const TIMEOUT_MS = 20_000;
const MAX_OUTPUT = 20_000_000;
export const convert = async (input: Buffer, width: number): Promise<Buffer> => {
const proc = Bun.spawn([process.execPath, WORKER, String(width)], {
stdin: new Blob([input]),
stdout: "pipe",
stderr: "pipe",
});
let timedOut = false;
const timer = setTimeout(() => {
timedOut = true;
proc.kill("SIGKILL");
}, TIMEOUT_MS);
try {
const [out, errText, code] = await Promise.all([
new Response(proc.stdout).arrayBuffer(),
new Response(proc.stderr).text(),
proc.exited,
]);
if (timedOut) throw new Error("image conversion timed out");
if (code !== 0 || proc.signalCode) {
// Exit 2 is the worker's own "sharp rejected this file"; anything else
// (signal, Bun panic banner, exit 1) is the decoder dying — log loudly,
// this is exactly the event the isolation exists for.
const reason = errText.trim().split("\n").filter(Boolean).slice(-1)[0] || "";
if (code !== 2)
warn(`IMAGE_WORKER_CRASH code=${code} signal=${proc.signalCode} ${reason.slice(0, 200)}`);
throw new Error(code === 2 && reason ? reason.slice(0, 200) : "image conversion failed");
}
if (!out.byteLength || out.byteLength > MAX_OUTPUT)
throw new Error("image conversion produced no output");
return Buffer.from(out);
} finally {
clearTimeout(timer);
}
};
|