From 684168ef2958782c0c7386dce90d042ba2446965 Mon Sep 17 00:00:00 2001 From: sundar Date: Thu, 23 Jul 2026 15:13:55 -0700 Subject: [PATCH] fix: resolve host-relative follow-up links against the origin, not baseUrl Mirrors ComfyPythonSDK#17: a serverless gateway serves the v2 contract under /deployment/{id}/api/v2 and returns job.urls.* links that already include that mount prefix; joining them to baseUrl doubled the prefix and 404'd every poll after submit. Server links (leading slash + containing /api/) now resolve against scheme+authority; internal shorthand paths and Cloud / self-hosted behavior are byte-identical to before. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KV7wUb2hyzKHsR52ZJTvAm --- src/low/followUpLinks.test.ts | 79 +++++++++++++++++++++++++++++++++++ src/low/transport.ts | 11 ++++- 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/low/followUpLinks.test.ts diff --git a/src/low/followUpLinks.test.ts b/src/low/followUpLinks.test.ts new file mode 100644 index 0000000..f53a869 --- /dev/null +++ b/src/low/followUpLinks.test.ts @@ -0,0 +1,79 @@ +/** + * Follow-up links (`job.urls.*`) resolve against the origin, not baseUrl. + * + * A server mounts the v2 contract wherever it likes — the serverless gateway + * serves it under `/deployment/{id}/api/v2` — and its host-relative follow-up + * links already include that mount prefix. Resolving them against `baseUrl` + * (which carries the same prefix) doubles it and 404s; they must resolve + * against the scheme+authority only. Internal shorthand paths (`/jobs/…`, + * `/assets…`) keep resolving under `baseUrl` + `/api/v2`. + */ + +import { describe, expect, it } from "vitest"; + +import { ComfyLow } from "./transport.js"; + +const GATEWAY_BASE = "https://stagingplatformapi.comfy.org/deployment/dep_123"; + +function jobJson(id: string, urlsPrefix: string) { + return { + id, + status: "queued", + created_at: "2026-07-10T18:20:00Z", + started_at: null, + completed_at: null, + expires_at: "2026-07-11T18:20:00Z", + queue_position: 0, + progress: null, + outputs: [], + error: null, + metrics: null, + urls: { + self: `${urlsPrefix}/api/v2/jobs/${id}`, + events: `${urlsPrefix}/api/v2/jobs/${id}/events`, + cancel: `${urlsPrefix}/api/v2/jobs/${id}/cancel`, + }, + }; +} + +function capturingLow(baseUrl: string, apiKey?: string) { + const requests: { url: string; auth: string | null }[] = []; + const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input instanceof Request ? input.url : String(input); + const headers = new Headers(input instanceof Request ? input.headers : init?.headers); + requests.push({ url, auth: headers.get("Authorization") }); + return new Response(JSON.stringify(jobJson("j1", "/deployment/dep_123")), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }) as typeof fetch; + return { low: new ComfyLow(baseUrl, apiKey, { fetch: fetchImpl }), requests }; +} + +describe("follow-up link resolution", () => { + it("resolves a gateway self link against the origin, not baseUrl", async () => { + const { low, requests } = capturingLow(GATEWAY_BASE, "comfyui-k"); + await low.getJob("/deployment/dep_123/api/v2/jobs/j1"); + expect(requests[0].url).toBe( + "https://stagingplatformapi.comfy.org/deployment/dep_123/api/v2/jobs/j1", + ); + }); + + it("keeps the deployment prefix for internal shorthand paths", async () => { + const { low, requests } = capturingLow(GATEWAY_BASE, "comfyui-k"); + await low.getJob("j1"); + expect(requests[0].url).toBe(`${GATEWAY_BASE}/api/v2/jobs/j1`); + }); + + it("leaves bare-surface self links unchanged", async () => { + const { low, requests } = capturingLow("https://api.comfy.org", "comfyui-k"); + await low.getJob("/api/v2/jobs/j1"); + expect(requests[0].url).toBe("https://api.comfy.org/api/v2/jobs/j1"); + }); + + it("still attaches auth to origin-resolved links", async () => { + const { low, requests } = capturingLow(GATEWAY_BASE, "comfyui-k"); + await low.getJob("/deployment/dep_123/api/v2/jobs/j1"); + expect(requests[0].auth).toBe("Bearer comfyui-k"); + }); +}); diff --git a/src/low/transport.ts b/src/low/transport.ts index bdb7961..28895b1 100644 --- a/src/low/transport.ts +++ b/src/low/transport.ts @@ -142,7 +142,16 @@ export class ComfyLow { private urlFor(path: string): string { if (path.startsWith("http")) return path; - if (path.startsWith("/api/")) return this.baseUrl + path; + // A server link (job.urls.*, marked by containing /api/) already carries + // the server's mount prefix, so it resolves against the origin — joining + // it to baseUrl would double the prefix on a prefix-mounted surface. + if (path.startsWith("/") && path.includes("/api/")) { + try { + return new URL(this.baseUrl).origin + path; + } catch { + return this.baseUrl + path; + } + } return this.baseUrl + API_PREFIX + path; }