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

  1. Retry the request after a short backoff — the condition is by definition transient (a new connection gets a new stream).
  2. For streaming bodies, buffer the body if the request is idempotent, so Bun's built-in h2 retry can engage.
  3. Reduce burst concurrency or spread requests across connections when the server's max-concurrent-streams is the limiter.
  4. 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

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


AI-assisted analysis of oven-sh/bun@8c5296ac45 (2026-08-16). Data as JSON: /api/errors/3b2355c28089b3c0. Report an issue: GitHub.