oven-sh/bun · warning · TypeError
HTTP2RefusedStream
HTTP2RefusedStream
Error message
HTTP2RefusedStream
What it means
The HTTP/2 server refused the stream: either an RST_STREAM with error code REFUSED_STREAM (src/http/h2_client/dispatch.rs:505-507) or a graceful GOAWAY whose last-stream-id excludes this stream (src/http/h2_client/dispatch.rs:529-534). Per RFC 9113 this signals 'retry on a new connection'. Bun already retries transparently when safe — no response bytes seen, buffered body, retries < MAX_H2_RETRIES (src/http/h2_client/ClientSession.rs:1099-1111) — so seeing the error means retries were exhausted, the body was a stream, or headers had already arrived.
Source
Thrown at src/http/error.rs:10
#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum Error {
#[error("CompressionFailed")]
CompressionFailed,
#[error("Aborted")]
Aborted,
#[error("WriteFailed")]
WriteFailed,
#[error("HTTP2RefusedStream")]
HTTP2RefusedStream,
#[error("HTTP2ContentLengthMismatch")]
HTTP2ContentLengthMismatch,
#[error("HTTP2FrameSizeError")]
HTTP2FrameSizeError,
#[error("HTTP2ProtocolError")]
HTTP2ProtocolError,
#[error("HTTP2FlowControlError")]
HTTP2FlowControlError,
#[error("HTTP2EnhanceYourCalm")]
HTTP2EnhanceYourCalm,
#[error("HTTP2HeaderListTooLarge")]
HTTP2HeaderListTooLarge,
#[error("HTTP2StreamReset")]
HTTP2StreamReset,
#[error("HTTP2GoAway")]
HTTP2GoAway,
#[error("HTTP2CompressionError")]View on GitHub (pinned to 8c5296ac45)
Solutions
- Retry the request after a short backoff — the condition is by definition transient (a new connection gets a new stream).
- For streaming bodies, buffer the body if the request is idempotent, so Bun's built-in h2 retry can engage.
- Reduce burst concurrency or spread requests across connections when the server's max-concurrent-streams is the limiter.
- If it persists against one host, check its drain/timeout settings (e.g. Envoy http2_max_requests, graceful shutdown windows).
Example fix
// before: single attempt on a streaming body
await fetch(url, { method: 'POST', body: stream, keepalive: true });
// after: buffered body + caller-side retry for refused streams
const buf = await new Response(stream).arrayBuffer();
for (let i = 0; ; i++) {
try { return await fetch(url, { method: 'POST', body: buf }); }
catch (e) {
if (i >= 2 || !String(e.message).includes('HTTP2RefusedStream')) throw e;
await Bun.sleep(50 * 2 ** i);
}
} Defensive patterns
Strategy: retry
Type guard
function isRefusedStream(err) {
return /HTTP2RefusedStream/i.test(String(err?.message ?? err));
} Try / catch
async function fetchRetryRefused(url, init, tries = 3) {
for (let i = 0; ;i++) {
try { return await fetch(url, init); }
catch (err) {
if (i >= tries - 1 || !isRefusedStream(err)) throw err;
await Bun.sleep(50 * 2 ** i); // server draining — new connection next try
}
}
} Prevention
- Buffer request bodies when possible so Bun's built-in h2 refused-stream retry can engage
- Add jittered retry with backoff around fetches during known deploy windows
- Watch for systemic causes: server max-concurrent-streams too low, LB draining too aggressively
When it happens
Trigger: Hitting a server mid-graceful-shutdown or at its max-concurrent-streams limit repeatedly; long-lived h2 connections during server rolling deploys; streamed request bodies (not retryable by Bun) refused at stream-open.
Common situations: Deploy churn behind ALBs/Envoy that drain h2 connections; servers configured with very low SETTINGS_MAX_CONCURRENT_STREAMS; bursty clients outrunning stream capacity on shared connections.
Related errors
- CompressionFailed
- Aborted
- WriteFailed
- HTTP2ContentLengthMismatch
- [azure] Failed to get public IP for ${vmName}
AI-assisted analysis of oven-sh/bun@8c5296ac45 (2026-08-16).
Data as JSON: /api/errors/3b2355c28089b3c0.
Report an issue: GitHub.