fix(ai): retry HTTP/2 pending stream cancellation

Bedrock's HTTP/2 connection can stall until the SDK's 5-minute session
timeout, which fails the request with "The pending stream has been
canceled" (ERR_HTTP2_STREAM_CANCEL). The request was never sent, so it
is safe to retry, but the message was only retried when a DNS cause was
appended.

closes #10379
This commit is contained in:
David Brailovsky 2026-10-05 13:47:22 +02:00
parent b78e6a9085
commit 5b6c792b42
3 changed files with 15 additions and 0 deletions

View file

@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed Bedrock requests that fail with `The pending stream has been canceled` after a stalled HTTP/2 connection not being retried automatically ([#10379](https://github.com/earendil-works/pi/issues/10379))
## [1.0.3] - 2026-10-05
### Breaking Changes

View file

@ -81,6 +81,9 @@ const RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([
"stream ended before message_stop",
"stream ended before a terminal response event",
"http2 request did not get a response",
// Node ERR_HTTP2_STREAM_CANCEL: the HTTP/2 session died before the request was
// sent, e.g. after the Bedrock SDK's 5-minute session timeout (#10379).
"pending stream has been canceled",
// Provider-requested retry delay cap failures should flow through the outer
// retry policy so callers can surface/abort the backoff (#1123).

View file

@ -62,6 +62,14 @@ describe("provider retry classification", () => {
expect(isRetryableAssistantError(fauxAssistantMessage("", { stopReason: "error", errorMessage }))).toBe(true);
});
it.each(["The pending stream has been canceled", "The pending stream has been canceled (caused by: socket closed)"])(
"matches HTTP/2 pending stream cancellation: %s",
(errorMessage) => {
// Regression for #10379.
expect(isRetryableAssistantError(fauxAssistantMessage("", { stopReason: "error", errorMessage }))).toBe(true);
},
);
it("matches OpenAI Responses streams that end before terminal events", () => {
expect(
isRetryableAssistantError(