fix(chatcore): report string-reason client aborts as 499, not 502 (#7907) (#8011)

abort(reason) rejects the upstream fetch with the raw reason, which is
often a bare string ("request_signal_aborted", "Client disconnected: ...")
carrying no `name` or `status`. The chatCore catch block only recognized
`error.name === "AbortError"`, so those aborts fell through to the 502
provider-failure default and were surfaced as `FAILED 502 / Bad Gateway`
in the client response, request logs, and usage records.

Classify the caught error with the existing isLocalStreamLifecycleError
helper (expanded by #7908 to cover AbortError plus the known abort reason
strings) so every client-abort shape maps to `499 Request aborted`.

Status-normalization follow-up to #7908, which already excluded these
aborts from provider circuit-breaker and cooldown accounting.

Co-authored-by: xiaolong.835 <xiaolong.835@bytedance.com>
This commit is contained in:
Long-Feeds
2026-07-22 13:35:18 +08:00
committed by GitHub
parent 5dd3c76ad7
commit 1699933326
3 changed files with 40 additions and 13 deletions

View File

@@ -0,0 +1 @@
- **Chat Core**: client aborts that reject the upstream fetch with a raw string reason (e.g. `request_signal_aborted`, `Client disconnected`) are now reported as `499 Request aborted` in the response, request logs, and usage records instead of `FAILED 502 / Bad Gateway`. Follow-up to #7908 for #7907.

View File

@@ -319,6 +319,7 @@ import {
stripMarkdownCodeFence,
} from "../utils/aiSdkCompat.ts";
import { generateRequestId } from "@/shared/utils/requestId";
import { isLocalStreamLifecycleError } from "@/shared/utils/circuitBreaker";
import { extractFacts } from "@/lib/memory/extraction";
import { handleToolCallExecution } from "@/lib/skills/interception";
import { OMNIROUTE_RESPONSE_HEADERS } from "@/shared/constants/headers";
@@ -3112,18 +3113,21 @@ export async function handleChatCore({
errorCode: error.code,
};
}
const failureStatus =
error.name === "AbortError"
? 499
: error.name === "TimeoutError" || error.name === "BodyTimeoutError"
? HTTP_STATUS.GATEWAY_TIMEOUT
: error.status && typeof error.status === "number"
? error.status
: HTTP_STATUS.BAD_GATEWAY;
const failureMessage =
error.name === "AbortError"
? "Request aborted"
: formatProviderError(error, provider, model, failureStatus);
// abort(reason) can reject the upstream fetch with a raw string reason
// (e.g. "request_signal_aborted") that has no `name`/`status`; classify
// via isLocalStreamLifecycleError so those map to 499 instead of falling
// through to the 502 provider-failure default.
const isRequestAborted = isLocalStreamLifecycleError(error);
const failureStatus = isRequestAborted
? 499
: error.name === "TimeoutError" || error.name === "BodyTimeoutError"
? HTTP_STATUS.GATEWAY_TIMEOUT
: error.status && typeof error.status === "number"
? error.status
: HTTP_STATUS.BAD_GATEWAY;
const failureMessage = isRequestAborted
? "Request aborted"
: formatProviderError(error, provider, model, failureStatus);
const upstreamErrorCode = getUpstreamErrorIdentifier(error);
// Tag our own deadline timeouts (fetch-start TimeoutError / body BodyTimeoutError,
// both surfaced as a 504) as "upstream_timeout" so the cooldown layer can tell a
@@ -3152,7 +3156,7 @@ export async function handleChatCore({
claudeCacheMeta: claudePromptCacheLogMeta,
cacheSource: "upstream",
});
if (error.name === "AbortError") {
if (isRequestAborted) {
streamController.handleError(error);
return createErrorResult(499, "Request aborted");
}

View File

@@ -2367,6 +2367,28 @@ test("chatCore maps upstream aborts to request-aborted errors", async () => {
assert.equal(result.error, "Request aborted");
});
test("chatCore maps raw string abort reasons to 499, not 502 (#7907)", async () => {
// abort(reason) rejects the upstream fetch with the raw reason — often a
// bare string with no `name`/`status`. It must map to 499 like a named
// AbortError, not fall through to the 502 provider-failure default.
const { result } = await invokeChatCore({
provider: "openai",
model: "gpt-4o-mini",
body: {
model: "gpt-4o-mini",
stream: false,
messages: [{ role: "user", content: "abort me with a string reason" }],
},
responseFactory() {
throw "request_signal_aborted";
},
});
assert.equal(result.success, false);
assert.equal(result.status, 499);
assert.equal(result.error, "Request aborted");
});
test("chatCore returns streaming responses without waiting for upstream completion", async () => {
const encoder = new TextEncoder();
let closeUpstream: (() => void) | null = null;