-
-
Notifications
You must be signed in to change notification settings - Fork 347
fix(proxy): exclude upstream 400/422 from breaker #1228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -135,6 +135,10 @@ function decodeRequestBodyAsJson(body: BodyInit | undefined): Record<string, unk | |
| } | ||
| } | ||
|
|
||
| function isNonRetryableUpstreamRequestError(error: Error): error is ProxyError { | ||
| return error instanceof ProxyError && (error.statusCode === 400 || error.statusCode === 422); | ||
| } | ||
|
|
||
| const OUTBOUND_TRANSPORT_HEADER_BLACKLIST = [ | ||
| "content-length", | ||
| "connection", | ||
|
|
@@ -1684,6 +1688,13 @@ export class ProxyForwarder { | |
| } | ||
| } | ||
|
|
||
| if ( | ||
| errorCategory === ErrorCategory.PROVIDER_ERROR && | ||
| isNonRetryableUpstreamRequestError(lastError) | ||
| ) { | ||
| errorCategory = ErrorCategory.NON_RETRYABLE_CLIENT_ERROR; | ||
| } | ||
|
Comment on lines
+1691
to
+1696
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 400/422 仅在非 hedge 路径重分类,流式请求仍可能误重试并计入熔断。 这段改动只覆盖 建议补丁diff --git a/src/app/v1/_lib/proxy/forwarder.ts b/src/app/v1/_lib/proxy/forwarder.ts
@@
if (reactiveRectifierResult.matched) {
@@
}
+
+ if (
+ errorCategory === ErrorCategory.PROVIDER_ERROR &&
+ isNonRetryableUpstreamRequestError(error)
+ ) {
+ errorCategory = ErrorCategory.NON_RETRYABLE_CLIENT_ERROR;
+ lastErrorCategory = errorCategory;
+ }
if (errorCategory === ErrorCategory.NON_RETRYABLE_CLIENT_ERROR) {🤖 Prompt for AI Agents |
||
|
|
||
| // ⭐ 3. 不可重试的客户端输入错误处理(不计入熔断器,不重试,立即返回) | ||
| if (errorCategory === ErrorCategory.NON_RETRYABLE_CLIENT_ERROR) { | ||
| const detectionResult = await getErrorDetectionResultAsync(lastError); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -894,6 +894,41 @@ describe("ProxyForwarder - retry limit enforcement", () => { | |
| }); | ||
| }); | ||
|
|
||
| describe("ProxyForwarder - client request upstream errors", () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| vi.mocked(categorizeErrorAsync).mockResolvedValue(ErrorCategory.PROVIDER_ERROR); | ||
| }); | ||
|
|
||
| test("upstream 400 should not retry or count against provider circuit breaker", async () => { | ||
| const session = createSession(); | ||
| const provider = createProvider({ | ||
| providerVendorId: null, | ||
| maxRetryAttempts: 4, | ||
| }); | ||
| session.setProvider(provider); | ||
|
|
||
| const doForward = vi.spyOn( | ||
| ProxyForwarder as unknown as { doForward: (...args: unknown[]) => unknown }, | ||
| "doForward" | ||
| ); | ||
| doForward.mockImplementationOnce(async () => { | ||
| throw new ProxyError("Provider returned 400: Bad Request", 400, { | ||
| body: '{"detail":"Bad Request"}', | ||
| providerId: provider.id, | ||
| providerName: provider.name, | ||
| }); | ||
| }); | ||
|
|
||
| await expect(ProxyForwarder.send(session)).rejects.toMatchObject({ | ||
| statusCode: 400, | ||
| }); | ||
|
|
||
| expect(doForward).toHaveBeenCalledTimes(1); | ||
| expect(mocks.recordFailure).not.toHaveBeenCalled(); | ||
| }); | ||
|
Comment on lines
+903
to
+929
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 建议补一条 422 用例,避免目标行为只被 400 半覆盖。 当前新增断言只覆盖 400;本次行为定义同时包含 422,建议并列增加 🤖 Prompt for AI Agents |
||
| }); | ||
|
|
||
| describe("ProxyForwarder - endpoint stickiness on retry", () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While this correctly maps 400/422 upstream errors to
NON_RETRYABLE_CLIENT_ERRORin the standard forwarding path, this mapping is completely missing in the streaming hedged path (sendStreamingWithHedge->handleAttemptFailure).As a result, if a streaming hedged request fails with an upstream 400 or 422 error:
PROVIDER_ERROR.recordFailure.launchAlternative()and attempt to failover/retry with other providers.To ensure consistent behavior across both paths, please apply this mapping in
handleAttemptFailure(around line 3910) as well, and add a corresponding test case inproxy-forwarder-retry-limit.test.tsto cover the hedged path.