From 11ea004e93d11ee47e193572e655cd02295af5a8 Mon Sep 17 00:00:00 2001 From: ding113 Date: Sun, 26 Apr 2026 07:20:58 +0000 Subject: [PATCH 1/2] test(proxy): document pre-aborted listener behavior --- .../v1/_lib/proxy/client-abort-listener.ts | 6 ++++ ...nse-handler-abort-listener-cleanup.test.ts | 28 +++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/app/v1/_lib/proxy/client-abort-listener.ts b/src/app/v1/_lib/proxy/client-abort-listener.ts index d47758907..7ebd24153 100644 --- a/src/app/v1/_lib/proxy/client-abort-listener.ts +++ b/src/app/v1/_lib/proxy/client-abort-listener.ts @@ -1,3 +1,9 @@ +/** + * 绑定客户端中止监听器,返回幂等清理函数。 + * + * 注意:如果传入的 signal 已经 aborted,会在当前调用栈同步执行 onAbort。 + * 调用方必须先初始化 onAbort 闭包会访问的控制器、任务 ID 等资源。 + */ export function bindClientAbortListener( signal: AbortSignal | null | undefined, onAbort: () => void diff --git a/tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts b/tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts index c5dda43b9..d7ebc1ea4 100644 --- a/tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts +++ b/tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts @@ -110,7 +110,7 @@ function makeProvider(overrides: Partial = {}): Provider { return { id: 99, name: "test-provider", - providerType: "openai", + providerType: "openai-compatible", baseUrl: "https://api.test.invalid", priority: 1, weight: 1, @@ -166,7 +166,7 @@ function makeSession(clientAbortSignal: AbortSignal | null, stream: boolean): Pr sessionId: null, requestSequence: 1, originalFormat: "openai", - providerType: "openai", + providerType: "openai-compatible", originalModelName: "gpt-5.4", originalUrlPathname: "/v1/chat/completions", providerChain: [], @@ -280,4 +280,28 @@ describe("ProxyResponseHandler client abort listener cleanup", () => { expect(removeSpy.mock.calls.filter(([type]) => type === "abort")).toHaveLength(0); expect(testState.cancelTask).toHaveBeenCalled(); }); + + it("invokes stream cancel once when client signal is already aborted", async () => { + const controller = new AbortController(); + controller.abort(); + const addSpy = vi.spyOn(controller.signal, "addEventListener"); + const removeSpy = vi.spyOn(controller.signal, "removeEventListener"); + const localAbortSpy = vi.spyOn(AbortController.prototype, "abort"); + const session = makeSession(controller.signal, true); + const upstreamResponse = new Response( + 'data: {"choices":[{"delta":{"content":"ok"}}]}\n\ndata: [DONE]\n\n', + { + headers: { "content-type": "text/event-stream" }, + } + ); + + const response = await ProxyResponseHandler.dispatch(session, upstreamResponse); + await response.text(); + await drainAsyncTasks(); + + expect(addSpy.mock.calls.filter(([type]) => type === "abort")).toHaveLength(0); + expect(removeSpy.mock.calls.filter(([type]) => type === "abort")).toHaveLength(0); + expect(testState.cancelTask).toHaveBeenCalledTimes(1); + expect(localAbortSpy).toHaveBeenCalledTimes(1); + }); }); From 1b5e31ff8fceb75896fcbf8fd15435111ad71816 Mon Sep 17 00:00:00 2001 From: ding113 Date: Sun, 26 Apr 2026 07:38:08 +0000 Subject: [PATCH 2/2] test(proxy): avoid global abort prototype spy --- .../unit/proxy/response-handler-abort-listener-cleanup.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts b/tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts index d7ebc1ea4..af0dd96f7 100644 --- a/tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts +++ b/tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts @@ -286,7 +286,6 @@ describe("ProxyResponseHandler client abort listener cleanup", () => { controller.abort(); const addSpy = vi.spyOn(controller.signal, "addEventListener"); const removeSpy = vi.spyOn(controller.signal, "removeEventListener"); - const localAbortSpy = vi.spyOn(AbortController.prototype, "abort"); const session = makeSession(controller.signal, true); const upstreamResponse = new Response( 'data: {"choices":[{"delta":{"content":"ok"}}]}\n\ndata: [DONE]\n\n', @@ -302,6 +301,5 @@ describe("ProxyResponseHandler client abort listener cleanup", () => { expect(addSpy.mock.calls.filter(([type]) => type === "abort")).toHaveLength(0); expect(removeSpy.mock.calls.filter(([type]) => type === "abort")).toHaveLength(0); expect(testState.cancelTask).toHaveBeenCalledTimes(1); - expect(localAbortSpy).toHaveBeenCalledTimes(1); }); });