Skip to content

Commit 65e2a33

Browse files
committed
fix(api): clear bedrock createMessage request timeout in finally
When a caller stops consuming the generator early (break/destroy), the generator enters the finally block without reaching the stream-completion timeout-clearing path, leaving the 10-minute request timer active and retaining the request controller until it expires. Clear the timeout at the start of the finally block, before the abort-listener removal. Test: createMessage regression - the generator is terminated early mid-stream and the 10-minute timer handle (captured via typed spies on setTimeout/clearTimeout) is asserted to have been cleared.
1 parent 5cd34ea commit 65e2a33

2 files changed

Lines changed: 58 additions & 0 deletions

File tree

‎src/api/providers/__tests__/bedrock.spec.ts‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2257,6 +2257,59 @@ describe("AwsBedrockHandler", () => {
22572257

22582258
firstRemoveSpy.mockRestore()
22592259
})
2260+
2261+
it("createMessage should clear the request timeout when the generator is terminated early", async () => {
2262+
const handler = new AwsBedrockHandler({
2263+
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
2264+
awsAccessKey: "test-access-key",
2265+
awsSecretKey: "test-secret-key",
2266+
awsRegion: "us-east-1",
2267+
})
2268+
2269+
// The stream yields one chunk and then hangs until released, so the
2270+
// generator is suspended mid-stream when it is terminated early.
2271+
let release: (() => void) | undefined
2272+
const pendingChunk = new Promise<void>((resolve) => {
2273+
release = resolve
2274+
})
2275+
const mockStream = async function* (): AsyncGenerator<string> {
2276+
yield JSON.stringify({ contentBlockDelta: { delta: { text: "hello" } } })
2277+
await pendingChunk
2278+
}
2279+
const mockSend = vi.fn().mockImplementation(() => Promise.resolve({ stream: mockStream() }))
2280+
handler["client"].send = mockSend
2281+
2282+
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout")
2283+
const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout")
2284+
2285+
const generator = handler.createMessage(
2286+
"You are a helpful assistant",
2287+
[{ role: "user", content: "Hello" }],
2288+
makeCreateMessageMetadata(),
2289+
)
2290+
2291+
// Consume the first chunk; the generator is now suspended mid-stream
2292+
const firstResult = await generator.next()
2293+
expect(firstResult.done).toBe(false)
2294+
expect(firstResult.value).toEqual({ type: "text", text: "hello" })
2295+
2296+
// Terminate the generator early (before the stream completes)
2297+
const returnPromise = generator.return(undefined)
2298+
release?.()
2299+
await returnPromise
2300+
2301+
// The 10-minute request timer scheduled by createMessage must have been cleared
2302+
const timerIndex = setTimeoutSpy.mock.calls.findIndex(([, delay]) => delay === 10 * 60 * 1000)
2303+
expect(timerIndex).toBeGreaterThanOrEqual(0)
2304+
const timeoutHandle: NodeJS.Timeout | undefined = setTimeoutSpy.mock.results[timerIndex]?.value
2305+
expect(timeoutHandle).toBeDefined()
2306+
expect(clearTimeoutSpy).toHaveBeenCalledWith(timeoutHandle)
2307+
2308+
// Belt and braces: make sure no real 10-minute timer survives the test
2309+
setTimeoutSpy.mockRestore()
2310+
clearTimeoutSpy.mockRestore()
2311+
clearTimeout(timeoutHandle)
2312+
})
22602313
})
22612314
})
22622315
})

‎src/api/providers/bedrock.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,11 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
838838
throw new Error("An unknown error occurred")
839839
}
840840
} finally {
841+
// Clear the request timeout as soon as the generator ends. This also covers
842+
// early termination by the caller (break/destroy), which bypasses the normal
843+
// timeout-clearing path after the stream completes.
844+
clearTimeout(timeoutId)
845+
841846
// Detach the bridge listener once the request ends (success or error) so the
842847
// external signal keeps no reference to this request's controller.
843848
if (abortListener) {

0 commit comments

Comments
 (0)