denoland/deno · error
no h2 capacity
Error message
no h2 capacity
What it means
Companion guard on the same WebSocket-over-HTTP/2 write path: after capacity was successfully reserved, send.capacity() is re-read; if it is 0 the write ends immediately with ErrorKind::WriteZero and this message. It marks an h2 edge where reserved capacity vanished (stream closing concurrently) so the code fails cleanly instead of sending an empty DATA frame.
Source
Thrown at ext/websocket/stream.rs:128
ErrorKind::ConnectionReset,
e,
)));
}
None => {
// The h2 stream is closed (typically a peer-sent
// RST_STREAM). Surface as a write error instead of
// panicking on `capacity() == 0` below. See #33953.
return Poll::Ready(Err(std::io::Error::new(
ErrorKind::ConnectionReset,
"h2 stream closed",
)));
}
}
// We'll try to send whatever we have capacity for.
let size = std::cmp::min(buf.len(), send.capacity());
if size == 0 {
return Poll::Ready(Err(std::io::Error::new(
ErrorKind::WriteZero,
"no h2 capacity",
)));
}
let buf: Bytes = Bytes::copy_from_slice(&buf[0..size]);
let len = buf.len();
// TODO(mmastrac): surface the h2 error?
let res = send
.send_data(buf, false)
.map_err(|_| std::io::Error::from(ErrorKind::Other));
Poll::Ready(res.map(|_| len))
}
}
}
fn poll_flush(
mut self: Pin<&mut Self>,View on GitHub (pinned to 9ad36f7a2c)
Solutions
- Handle WriteZero exactly like ConnectionReset: close the socket and reconnect.
- Serialize writes through a queue that is flushed/aborted on close so no write races teardown.
- Check readyState and use a closed flag set in onclose/onerror before each send.
- Reduce end-of-life chatter: stop heartbeats/acks as soon as close handshake begins.
Example fix
// before
const ok = ws.send(frame); // may throw WriteZero: no h2 capacity during close race
// after
if (!open) return; // open flag cleared in onclose/onerror
try { ws.send(frame); }
catch (e) { open = false; ws.close(); reconnectLater(); } Defensive patterns
Strategy: try-catch
Validate before calling
if (!open || ws.readyState !== WebSocket.OPEN) return; // skip send entirely during teardown
Type guard
const isCapacityError = (e) => e instanceof Error && (/no h2 capacity/.test(e.message) || String(e.code ?? "").includes("EPIPE")); Try / catch
try { ws.send(frame); } catch (e) { if (/no h2 capacity/.test(String(e.message))) { open = false; ws.close(); reconnectLater(); } else throw e; } Prevention
- Serialize sends through one queue drained by a single writer so teardown aborts cleanly.
- Stop heartbeats/acks as soon as the close handshake begins.
- Never fire-and-forget writes right before intentional close().
When it happens
Trigger: A WS-over-h2 write racing stream closure: capacity poll succeeded, then RST_STREAM/teardown drained the window before send_data, producing a zero-capacity write. Same triggers as 'h2 stream closed' but hitting the narrower window.
Common situations: Teardown races under load (burst of sends as the peer closes); proxies resetting streams mid-write; reconnect storms where old sockets are still being written to.
Related errors
- h2 stream closed
- ERR_HTTP2_STATUS_101
- Already upgraded
- Already closed
- Unsupported 'alpnProtocols' option provided. 'h2' and 'http/
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/d445ffac79bf912b.
Report an issue: GitHub.