Node 24.21 MIMEType.parse: validate a Sume artifact content type
Node 24.21.0 adds a non-throwing MIMEType.parse. Use it, with a try/catch fallback for older Node, to check a Sume artifact's content_type before saving.

To check the content type of a file a Sume job returned, parse the artifact's content_type with util.MIMEType and compare its type and essence. On Node 24.21.0, a MIMEType.parse() that does not throw is available for this (Node.js 24.21.0 release notes, read 2026-10-04). On earlier Node lines, new MIMEType(...) inside a try block does the same job, so the code below works on both.
The release is the September 8 LTS update. Among the items in its notes are OpenSSL 3.5.8, Undici 7.29.1 and the non-throwing parse. Only the last one matters here: it turns a malformed header from an exception into a value you can branch on.
Where the content type comes from
A completed Sume job lists artifacts in result.artifacts. Each has an id, a url on media.sume.com, a type such as image, and a content_type such as image/png (Jobs and results). You asked for an image, so the check is cheap insurance: before writing bytes to a path with an extension, confirm the declared type matches what you expect to store.
The image models accept an output_format of png, jpeg, webp or svg (Image models), so the extension is not fixed by your pipeline. Deriving it from the artifact's declared type, instead of from what you asked for, keeps a file's name honest. Some model responses name the field media_type rather than content_type, so read whichever your route returns.
A parse helper that works on Node 22 and 24
The helper below prefers the non-throwing static method when it exists and otherwise falls back to the constructor. It returns null for anything that is not a valid MIME type, and the caller maps the essence to a file extension.
import { MIMEType } from "node:util";
export function parseMime(value: unknown): MIMEType | null {
if (typeof value !== "string") return null;
const anyMime = MIMEType as unknown as { parse?: (v: string) => MIMEType | null };
if (typeof anyMime.parse === "function") return anyMime.parse(value) ?? null;
try {
return new MIMEType(value);
} catch {
return null;
}
}
const EXT: Record<string, string> = {
"image/png": "png",
"image/jpeg": "jpg",
"image/webp": "webp",
"video/mp4": "mp4",
"audio/mpeg": "mp3",
};
export function extensionFor(artifact: { type?: string; content_type?: string }) {
const mime = parseMime(artifact.content_type);
if (!mime) return null;
if (artifact.type && mime.type !== artifact.type) return null;
return EXT[mime.essence] ?? null;
}
console.log(extensionFor({ type: "image", content_type: "image/png" }));
console.log(extensionFor({ type: "image", content_type: "not a mime" }));What to do on a mismatch
- Do not guess an extension. Store the file under the artifact id, log the
job_id, and flag it for review. - Keep the original
urlfrom the job record rather than rebuilding it, since Sume media URLs from the result are the public output. - Pin Node in CI so the branch you test is the branch you ship. The code above runs on both paths, but only one is exercised per runtime.
- Treat a missing
content_typeas unknown, not as a default image type.
Sources
Related posts
More in Developers
- Which Node versions to test the Sume SDK on: 22, 24 and 26
Node 26.10.0 is Current, 24.21.0 and 22.23.3 are LTS. A small CI matrix and smoke test for code that calls the Sume API with fetch and WebCrypto.
- Node 26.10 fs.openAsBlobSync: upload a local file with Sume uploadFile
Node 26.10 adds fs.openAsBlobSync. Open a file as a typed Blob and pass it to the Sume SDK's uploadFile to get a durable HTTPS URL for a Format input.
- Node 26.10 SQLite binds undefined as NULL: a Sume webhook job table
Node 26.10 binds undefined as NULL in node:sqlite. A Sume job-webhook handler can still be explicit with null and dedupe on job_id without a driver.
- OpenAI Agents API hosted sandbox: which Sume hosts to allow
OpenAI's Agents API is in public beta with hosted or connected sandboxes. Which Sume hosts to allow, how to pass the MCP URL, and why the key stays in a secret.
Written by Sume