From 309af00fe3dff5c9c171bf3540ee73ca0a3a71b6 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Thu, 29 Oct 2026 02:49:44 -0400 Subject: [PATCH] fix(api): normalize media types when discovering completed outputs --- lib/comfy.ts | 8 ++++++-- test/api-output-mime.test.ts | 39 ++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 test/api-output-mime.test.ts diff --git a/lib/comfy.ts b/lib/comfy.ts index dc852ef..518c328 100644 --- a/lib/comfy.ts +++ b/lib/comfy.ts @@ -79,9 +79,13 @@ function jobErrorMessage(error: unknown) { return message || "ComfyUI could not run this workflow. Check the ComfyUI console."; } +function baseMediaType(value: string) { + return value.split(";", 1)[0].trim().toLowerCase(); +} + export function publicJob(job: Job): PublicJob { - const video = job.outputs.find((output) => output.contentType.startsWith("video/")); - const data = job.outputs.find((output) => output.contentType === "application/json"); + const video = job.outputs.find((output) => baseMediaType(output.contentType).startsWith("video/")); + const data = job.outputs.find((output) => baseMediaType(output.contentType) === "application/json"); return { id: job.id, status: job.status, diff --git a/test/api-output-mime.test.ts b/test/api-output-mime.test.ts new file mode 100644 index 0000000..ebc2dc6 --- /dev/null +++ b/test/api-output-mime.test.ts @@ -0,0 +1,39 @@ +import assert from "node:assert/strict"; +import { once } from "node:events"; +import { createServer } from "node:http"; +import type { AddressInfo } from "node:net"; +import { register } from "node:module"; +import test from "node:test"; +register("data:text/javascript,export const resolve = (specifier, context, next) => next(specifier === 'next/server' ? 'next/server.js' : specifier, context);"); +const { GET } = await import("../app/api/jobs/[id]/route.ts"); +for (const [videoType, jsonType] of [ + ["video/mp4", "application/json"], + ["VIDEO/MP4", "Application/JSON"], + ['video/mp4; codecs="avc1"', "application/json; charset=utf-8"], + [' Video/MP4 ; codecs="avc1"', " Application/JSON ; charset=utf-8"], +] as const) test(`completed output discovery recognizes ${videoType} and ${jsonType}`, async (t) => { + const server = createServer((_request, response) => response.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ + id: "job_1", status: "succeeded", urls: {}, outputs: [ + { id: "video_1", name: "result.mp4", content_type: videoType, node_id: "67" }, + { id: "data_1", name: "tracks.json", content_type: jsonType, node_id: "6" }, + ], + }))); + server.listen(0, "127.0.0.1"); await once(server, "listening"); + process.env.COMFY_BASE_URL = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; + delete process.env.OPENJUTSU_PASSWORD; delete process.env.COMFY_API_KEY; + t.after(() => { server.closeAllConnections(); server.close(); }); + const response = await GET(new Request("http://localhost/api/jobs/job_1"), { params: Promise.resolve({ id: "job_1" }) }); + assert.equal(response.status, 200); + const job = await response.json(); + assert.deepEqual(job.video, { id: "video_1", name: "result.mp4", url: "/api/jobs/job_1/outputs/video_1" }); + assert.equal(job.data, "/api/jobs/job_1/outputs/data_1"); +}); +test("unrelated images and structured error documents do not become video or track results", async (t) => { + t.mock.method(globalThis, "fetch", async () => Response.json({ id: "job_1", status: "succeeded", urls: {}, outputs: [ + { id: "image_1", name: "preview.png", content_type: "image/png" }, + { id: "problem_1", name: "problem.json", content_type: "application/problem+json" }, + ] })); + process.env.COMFY_BASE_URL = "http://127.0.0.1:8189"; + const response = await GET(new Request("http://localhost/api/jobs/job_1"), { params: Promise.resolve({ id: "job_1" }) }); + const job = await response.json(); assert.equal(job.video, null); assert.equal(job.data, null); +});