mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
fix: add AbortController timeout to fetchImageEndpoint (#3105)
* fix: add AbortController timeout to fetchImageEndpoint fetchImageEndpoint uses raw fetch() without timeout control. Long-running image generation requests (~20-30s) are killed by Next.js default timeout, producing upstream_error responses. Replace fetch() with fetchWithTimeout() from shared utils, defaulting to FETCH_TIMEOUT_MS (120s via OMNIROUTE_DEFAULT_FETCH_TIMEOUT_MS). * fix: return 504 for image fetch timeout, add tests Address review feedback from gemini-code-assist: - Import FetchTimeoutError and catch it in fetchImageEndpoint - Return 504 (Gateway Timeout) instead of 502 for timeout/AbortError - Add 3 focused unit tests for timeout, non-timeout, and success paths Clarifies timeout semantics: timeout is a gateway timeout (504), not a bad gateway (502). Non-timeout fetch errors remain 502. --------- Co-authored-by: mgarmash <mgarmash@37bytes.com>
This commit is contained in:
@@ -41,6 +41,7 @@ import {
|
||||
extractComfyOutputFiles,
|
||||
} from "../utils/comfyuiClient.ts";
|
||||
import { fetchRemoteImage } from "@/shared/network/remoteImageFetch";
|
||||
import { FetchTimeoutError, fetchWithTimeout, getConfiguredTimeout } from "@/shared/utils/fetchTimeout";
|
||||
import { sanitizeErrorMessage, sanitizeUpstreamDetails } from "../utils/error.ts";
|
||||
|
||||
interface KieImageOptions {
|
||||
@@ -2414,11 +2415,33 @@ function saveImageErrorResult({ provider, model, status, startTime, error, reque
|
||||
*/
|
||||
async function fetchImageEndpoint(url, headers, body, provider, log) {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
let response;
|
||||
try {
|
||||
response = await fetchWithTimeout(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
timeoutMs: getConfiguredTimeout(),
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const isAbortError =
|
||||
typeof err === "object" &&
|
||||
err !== null &&
|
||||
"name" in err &&
|
||||
(err as { name?: unknown }).name === "AbortError";
|
||||
if (err instanceof FetchTimeoutError || isAbortError) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
if (log) {
|
||||
log.error("IMAGE", `${provider} fetch error: ${message}`);
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
status: 504,
|
||||
error: `Image provider error: ${sanitizeErrorMessage(message || err)}`,
|
||||
};
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
@@ -2442,14 +2465,15 @@ async function fetchImageEndpoint(url, headers, body, provider, log) {
|
||||
data: data.data || [],
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
if (log) {
|
||||
log.error("IMAGE", `${provider} fetch error: ${err.message}`);
|
||||
log.error("IMAGE", `${provider} fetch error: ${message}`);
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`,
|
||||
error: `Image provider error: ${sanitizeErrorMessage(message || err)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
81
tests/unit/image-generation-fetch-timeout.test.ts
Normal file
81
tests/unit/image-generation-fetch-timeout.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { handleImageGeneration } = await import(
|
||||
"../../open-sse/handlers/imageGeneration.ts"
|
||||
);
|
||||
|
||||
function restore<T>(fn: () => T): T {
|
||||
const originalFetch = globalThis.fetch;
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
}
|
||||
|
||||
function makeAbortError() {
|
||||
return Object.create(Error.prototype, {
|
||||
message: { value: "The operation was aborted", writable: true, configurable: true },
|
||||
name: { value: "AbortError", writable: true, configurable: true },
|
||||
});
|
||||
}
|
||||
|
||||
test("fetch timeout in OpenAI provider path returns 504 and sanitized error", () =>
|
||||
restore(async () => {
|
||||
globalThis.fetch = async () => {
|
||||
throw makeAbortError();
|
||||
};
|
||||
|
||||
const result = await handleImageGeneration({
|
||||
body: { model: "openai/gpt-image-2", prompt: "timeout test" },
|
||||
credentials: { apiKey: "test-key" },
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 504);
|
||||
assert.match(result.error, /Image provider error:/);
|
||||
}));
|
||||
|
||||
test("non-timeout fetch error still returns 502", () =>
|
||||
restore(async () => {
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error("network down");
|
||||
};
|
||||
|
||||
const result = await handleImageGeneration({
|
||||
body: { model: "openai/gpt-image-2", prompt: "network error test" },
|
||||
credentials: { apiKey: "test-key" },
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 502);
|
||||
}));
|
||||
|
||||
test("successful image gen passes AbortSignal and returns URL", () =>
|
||||
restore(async () => {
|
||||
let seenSignal: AbortSignal | null = null;
|
||||
|
||||
globalThis.fetch = async (url, options) => {
|
||||
seenSignal = (options as RequestInit).signal ?? null;
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
created: 999,
|
||||
data: [{ url: "https://cdn.example.com/timeout-test.png" }],
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
};
|
||||
|
||||
const result = await handleImageGeneration({
|
||||
body: { model: "openai/gpt-image-2", prompt: "success test" },
|
||||
credentials: { apiKey: "test-key" },
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.ok(seenSignal, "AbortSignal should be passed through fetchWithTimeout");
|
||||
assert.equal(result.data.data[0].url, "https://cdn.example.com/timeout-test.png");
|
||||
}));
|
||||
Reference in New Issue
Block a user