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

  1. Handle WriteZero exactly like ConnectionReset: close the socket and reconnect.
  2. Serialize writes through a queue that is flushed/aborted on close so no write races teardown.
  3. Check readyState and use a closed flag set in onclose/onerror before each send.
  4. 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

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


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/d445ffac79bf912b. Report an issue: GitHub.