mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
fix(executors): pass TimeoutError reason to controller.abort() in 7 niche executors (#8699)
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
This commit is contained in:
committed by
GitHub
parent
7d67912f5b
commit
f2d7e9a783
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- **executors**: fix internal timeout misclassified as client disconnect (499) for 7 niche executors — pass TimeoutError reason to controller.abort() (#8197 side-finding)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.49] — TBD
|
||||
|
||||
@@ -401,7 +401,12 @@ export class DuckDuckGoWebExecutor extends BaseExecutor {
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||
const ddgTestMs = FETCH_TIMEOUT_MS;
|
||||
const timeout = setTimeout(() => {
|
||||
const err = new Error(`duckduckgo-web testConnection timeout after ${ddgTestMs}ms`);
|
||||
err.name = "TimeoutError";
|
||||
controller.abort(err);
|
||||
}, ddgTestMs);
|
||||
|
||||
const mergedSignal = signal
|
||||
? AbortSignal.any([signal, controller.signal])
|
||||
@@ -518,7 +523,12 @@ export class DuckDuckGoWebExecutor extends BaseExecutor {
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||
const ddgExecMs = FETCH_TIMEOUT_MS;
|
||||
const timeout = setTimeout(() => {
|
||||
const err = new Error(`duckduckgo-web execute timeout after ${ddgExecMs}ms`);
|
||||
err.name = "TimeoutError";
|
||||
controller.abort(err);
|
||||
}, ddgExecMs);
|
||||
const mergedSignal = signal
|
||||
? AbortSignal.any([signal, controller.signal])
|
||||
: controller.signal;
|
||||
|
||||
@@ -204,7 +204,12 @@ export class FeloWebExecutor extends BaseExecutor {
|
||||
signal?: AbortSignal
|
||||
): Promise<boolean> {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), this.getTimeoutMs());
|
||||
const feloTestMs = this.getTimeoutMs();
|
||||
const timeout = setTimeout(() => {
|
||||
const err = new Error(`felo-web testConnection timeout after ${feloTestMs}ms`);
|
||||
err.name = "TimeoutError";
|
||||
controller.abort(err);
|
||||
}, feloTestMs);
|
||||
try {
|
||||
const mergedSignal = signal
|
||||
? AbortSignal.any([signal, controller.signal])
|
||||
@@ -243,7 +248,12 @@ export class FeloWebExecutor extends BaseExecutor {
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), this.getTimeoutMs());
|
||||
const feloExecMs = this.getTimeoutMs();
|
||||
const timeout = setTimeout(() => {
|
||||
const err = new Error(`felo-web execute timeout after ${feloExecMs}ms`);
|
||||
err.name = "TimeoutError";
|
||||
controller.abort(err);
|
||||
}, feloExecMs);
|
||||
const mergedSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
||||
|
||||
try {
|
||||
|
||||
@@ -100,7 +100,12 @@ export async function firecrawlFetch(opts: FirecrawlScrapeOptions): Promise<WebF
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), getFirecrawlTimeoutMs());
|
||||
const firecrawlMs = getFirecrawlTimeoutMs();
|
||||
const timeoutId = setTimeout(() => {
|
||||
const err = new Error(`firecrawl-fetch timeout after ${firecrawlMs}ms`);
|
||||
err.name = "TimeoutError";
|
||||
controller.abort(err);
|
||||
}, firecrawlMs);
|
||||
|
||||
try {
|
||||
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
||||
|
||||
@@ -170,7 +170,11 @@ async function bootstrapJwt(
|
||||
|
||||
const url = `${baseUrl}${BOOTSTRAP_PATH}`;
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), BOOTSTRAP_TIMEOUT_MS);
|
||||
const timer = setTimeout(() => {
|
||||
const err = new Error(`mimocode bootstrap timeout after ${BOOTSTRAP_TIMEOUT_MS}ms`);
|
||||
err.name = "TimeoutError";
|
||||
controller.abort(err);
|
||||
}, BOOTSTRAP_TIMEOUT_MS);
|
||||
const onSignal = signal ? () => controller.abort(signal.reason) : null;
|
||||
if (signal && onSignal) signal.addEventListener("abort", onSignal, { once: true });
|
||||
|
||||
|
||||
@@ -40,7 +40,11 @@ export async function tavilyFetch(opts: TavilyFetchOptions): Promise<WebFetchRes
|
||||
};
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), TAVILY_TIMEOUT_MS);
|
||||
const timeoutId = setTimeout(() => {
|
||||
const err = new Error(`tavily-fetch timeout after ${TAVILY_TIMEOUT_MS}ms`);
|
||||
err.name = "TimeoutError";
|
||||
controller.abort(err);
|
||||
}, TAVILY_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const response = await fetch(TAVILY_EXTRACT_URL, {
|
||||
|
||||
@@ -187,7 +187,11 @@ async function directFetch(
|
||||
signal?: AbortSignal | null
|
||||
): Promise<Response> {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 120_000);
|
||||
const timer = setTimeout(() => {
|
||||
const err = new Error("theoldllm timeout after 120000ms");
|
||||
err.name = "TimeoutError";
|
||||
controller.abort(err);
|
||||
}, 120_000);
|
||||
const onSignal = signal ? () => controller.abort(signal.reason) : undefined;
|
||||
signal?.addEventListener("abort", onSignal!, { once: true });
|
||||
|
||||
|
||||
@@ -62,7 +62,11 @@ export async function tinyfishFetch(opts: TinyFishFetchOptions): Promise<WebFetc
|
||||
};
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), TINYFISH_TIMEOUT_MS);
|
||||
const timeoutId = setTimeout(() => {
|
||||
const err = new Error(`tinyfish-fetch timeout after ${TINYFISH_TIMEOUT_MS}ms`);
|
||||
err.name = "TimeoutError";
|
||||
controller.abort(err);
|
||||
}, TINYFISH_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const response = await fetch(TINYFISH_FETCH_URL, {
|
||||
|
||||
54
tests/unit/isLocalStreamLifecycleError-abort-shape.test.ts
Normal file
54
tests/unit/isLocalStreamLifecycleError-abort-shape.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { isLocalStreamLifecycleError } from "../../src/shared/utils/circuitBreaker.ts";
|
||||
|
||||
describe("isLocalStreamLifecycleError — AbortError shape discrimination", () => {
|
||||
// Case 1: bare abort() with no reason → AbortError → should return TRUE
|
||||
// (This is the current buggy behavior — internal timeouts produce AbortError
|
||||
// which is indistinguishable from a real client disconnect.)
|
||||
it("returns TRUE for bare AbortController.abort() with no reason", () => {
|
||||
const controller = new AbortController();
|
||||
controller.abort(); // no reason → DOMException { name: "AbortError" }
|
||||
// Simulate what Node does: the signal's reason is a DOMException
|
||||
const reason = controller.signal.reason;
|
||||
assert.equal(isLocalStreamLifecycleError(reason), true);
|
||||
});
|
||||
|
||||
// Case 2: abort(reason) where reason.name = "TimeoutError" → should return FALSE
|
||||
// (This is the fixed pattern from base.ts — internal timeouts should NOT be
|
||||
// treated as client disconnects.)
|
||||
it("returns FALSE for abort(reason) with name 'TimeoutError'", () => {
|
||||
const controller = new AbortController();
|
||||
const err = new Error("Fetch timeout after 120000ms on https://example.com");
|
||||
err.name = "TimeoutError";
|
||||
controller.abort(err);
|
||||
assert.equal(isLocalStreamLifecycleError(controller.signal.reason), false);
|
||||
});
|
||||
|
||||
// Case 3: abort(reason) where reason.name = "BodyTimeoutError" → should return FALSE
|
||||
it("returns FALSE for abort(reason) with name 'BodyTimeoutError'", () => {
|
||||
const controller = new AbortController();
|
||||
const err = new Error("Body Timeout Error");
|
||||
err.name = "BodyTimeoutError";
|
||||
controller.abort(err);
|
||||
assert.equal(isLocalStreamLifecycleError(controller.signal.reason), false);
|
||||
});
|
||||
|
||||
// Case 4: message "controller is already closed" → should return TRUE (existing)
|
||||
it("returns TRUE for message 'controller is already closed'", () => {
|
||||
const err = new Error("controller is already closed");
|
||||
assert.equal(isLocalStreamLifecycleError(err), true);
|
||||
});
|
||||
|
||||
// Case 5: message "request_signal_aborted" → should return TRUE (existing)
|
||||
it("returns TRUE for message 'request_signal_aborted'", () => {
|
||||
const err = new Error("request_signal_aborted");
|
||||
assert.equal(isLocalStreamLifecycleError(err), true);
|
||||
});
|
||||
|
||||
// Case 6: message "client disconnected" → should return TRUE (existing)
|
||||
it("returns TRUE for message 'client disconnected'", () => {
|
||||
const err = new Error("client disconnected");
|
||||
assert.equal(isLocalStreamLifecycleError(err), true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user