{"record":{"id":"3c47a07520ce305e","repo":"denoland/deno","slug":"h2-stream-closed","errorCode":null,"errorMessage":"h2 stream closed","messagePattern":"h2 stream closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"ext/websocket/stream.rs","lineNumber":118,"sourceCode":"        // Zero-length write succeeds\n        if buf.is_empty() {\n          return Poll::Ready(Ok(0));\n        }\n\n        send.reserve_capacity(buf.len());\n        match ready!(send.poll_capacity(cx)) {\n          Some(Ok(_)) => {} // capacity reserved\n          Some(Err(e)) => {\n            return Poll::Ready(Err(std::io::Error::new(\n              ErrorKind::ConnectionReset,\n              e,\n            )));\n          }\n          None => {\n            // The h2 stream is closed (typically a peer-sent\n            // RST_STREAM). Surface as a write error instead of\n            // panicking on `capacity() == 0` below. See #33953.\n            return Poll::Ready(Err(std::io::Error::new(\n              ErrorKind::ConnectionReset,\n              \"h2 stream closed\",\n            )));\n          }\n        }\n\n        // We'll try to send whatever we have capacity for.\n        let size = std::cmp::min(buf.len(), send.capacity());\n        if size == 0 {\n          return Poll::Ready(Err(std::io::Error::new(\n            ErrorKind::WriteZero,\n            \"no h2 capacity\",\n          )));\n        }\n\n        let buf: Bytes = Bytes::copy_from_slice(&buf[0..size]);\n        let len = buf.len();\n        // TODO(mmastrac): surface the h2 error?","sourceCodeStart":100,"sourceCodeEnd":136,"githubUrl":"https://github.com/denoland/deno/blob/9ad36f7a2cce60488e6ec52283efb32efddaf93a/ext/websocket/stream.rs#L100-L136","documentation":"Write path of Deno's WebSocket-over-HTTP/2 transport: before sending DATA frames it polls the h2 send stream for capacity. poll_capacity() returning None means the stream is closed — typically a peer-sent RST_STREAM or stream teardown — and the write completes with ErrorKind::ConnectionReset and this message instead of panicking (added for deno #33953).","triggerScenarios":"Calling ws.send() on a WebSocket riding HTTP/2 after the peer reset the stream or the connection entered GOAWAY teardown: server-side rejection/cleanup, proxy killing the stream, or sending after the remote already answered with a close/RST.","commonSituations":"Long-lived WS-over-h2 connections behind load balancers that reap idle streams; sending during teardown races where onclose has not fired yet; mobile clients dropping and proxies resetting streams; ALPN-negotiated h2 endpoints (CDNs) that time streams out aggressively.","solutions":["Treat any write error here as fatal for the socket: close the WebSocket and reconnect with backoff.","Stop sending once close starts: guard ws.send() with readyState === OPEN and queue/drop during CLOSING.","Send periodic app-level heartbeats so dead streams are detected via error or timeout instead of writes failing much later.","If you control the LB/proxy, raise stream idle timeouts or switch the endpoint to WebSocket-over-HTTP/1.1."],"exampleFix":"// before\nws.on(\"message\", m => ws.send(ack(m))); // send may hit RST_STREAM during teardown\n\n// after\nfunction safeSend(ws, data) {\n  if (ws.readyState !== WebSocket.OPEN) return false;\n  try { ws.send(data); return true; }\n  catch (e) { // ConnectionReset: h2 stream closed\n    ws.close();\n    scheduleReconnect();\n    return false;\n  }\n}","handlingStrategy":"try-catch","validationCode":"const isOpen = (ws) => ws.readyState === WebSocket.OPEN; // guard before every send","typeGuard":"const isFatalWsWriteError = (e) => e instanceof Error && (/h2 stream closed/.test(e.message) || /ConnectionReset|ECONNRESET/.test(String(e.code ?? e.cause ?? e.message)));","tryCatchPattern":"try { ws.send(frame); } catch (e) { if (/h2 stream closed/.test(String(e))) { ws.close(); scheduleReconnect(); } else throw e; }","preventionTips":["Maintain an `open` flag cleared in onclose/onerror and check it before every send.","Queue outbound frames during reconnect instead of writing to dying sockets.","Use heartbeats to detect dead h2 streams early.","Raise idle/stream timeouts on LBs fronting WS-over-h2 endpoints."],"tags":["websocket","http2","h2","connection-reset","streaming"],"backgroundTag":"connection-reset","analyzedSha":"9ad36f7a2cce60488e6ec52283efb32efddaf93a","analyzedAt":"2026-08-20T13:07:44.778Z","contentChangedAt":"2026-08-20T13:07:44.778Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}