an image's header, by path, because ffprobe cannot read a HEIC

`/v1/probe` is ffprobe, which is the wrong tool for a still and fails at it
loudly: a HEIC out of an iPhone is not a container it understands, so it rejects
the file outright with "moov atom not found" rather than reporting no
dimensions.

So anything needing a photograph's size had nowhere to ask — and a caller
building a width ladder needs exactly that, to know which rungs would be an
upscale.

sharp reads it, HEIC included, because `metadata()` only parses the header. That
is precisely the part of a HEIC that works; the pixels are what its bundled
libheif cannot reach, which is what decodable.ts routes around. No decode
happens, so this needs no job slot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-29 11:13:43 +01:00
co-authored by Claude Opus 5
parent 69f4cc9cae
commit 100eee5256
+33 -1
View File
@@ -1,6 +1,6 @@
import { timingSafeEqual } from "node:crypto"; import { timingSafeEqual } from "node:crypto";
import { ASSETS_DIR, MAX_CONCURRENT_JOBS, MAX_UPLOAD_BYTES, PORT, requireToken } from "./config"; import { ASSETS_DIR, MAX_CONCURRENT_JOBS, MAX_UPLOAD_BYTES, PORT, requireToken } from "./config";
import { ApiError, invalid, notFound, toErrorResponse } from "./errors"; import { ApiError, invalid, notFound, toErrorResponse, unprocessable } from "./errors";
import { contentPath, deleteAsset, putAsset, readAsset } from "./assets/store"; import { contentPath, deleteAsset, putAsset, readAsset } from "./assets/store";
import { ffprobe } from "./probe/ffprobe"; import { ffprobe } from "./probe/ffprobe";
import { runSharp, type SharpRequest } from "./sharp/run"; import { runSharp, type SharpRequest } from "./sharp/run";
@@ -225,6 +225,38 @@ async function route(request: Request): Promise<Response> {
return Response.json(await withSlot(() => ffprobe(target))); return Response.json(await withSlot(() => ffprobe(target)));
} }
/*
An image's header, by path.
Separate from `/v1/probe` because ffprobe is the wrong tool for a still and fails at it loudly: a
HEIC out of an iPhone is not a media container it understands, and it rejects the file outright
("moov atom not found") rather than reporting no dimensions. Anything that needs a photograph's
size — a caller building a width ladder, and therefore needing to know which rungs would be an
upscale — had nowhere to ask.
sharp reads it, including HEIC, because `metadata()` only parses the header. That is precisely the
part of a HEIC that works; the pixels are what its bundled libheif cannot reach, which is what
`decodable.ts` exists to route around. No decode happens here, so this is cheap and needs no job
slot: it is a file header read, not an image operation.
*/
if (path === "/v1/image" && request.method === "GET") {
const target = await safeInputPath(url.searchParams.get("path"), "path");
const meta = await sharp(target)
.metadata()
.catch(() => {
throw unprocessable(`Could not read that image: ${target}`);
});
return Response.json({
format: meta.format ?? null,
width: meta.width ?? null,
height: meta.height ?? null,
space: meta.space ?? null,
hasAlpha: meta.hasAlpha ?? false,
pages: meta.pages ?? 1,
orientation: meta.orientation ?? null,
});
}
const asset = path.match(/^\/v1\/assets\/([^/]+)(\/content|\/probe)?$/); const asset = path.match(/^\/v1\/assets\/([^/]+)(\/content|\/probe)?$/);
if (asset) { if (asset) {
const [, id, suffix] = asset; const [, id, suffix] = asset;