denoland/deno · error · Error
ERR_HTTP2_NO_SOCKET_MANIPULATION
ERR_HTTP2_NO_SOCKET_MANIPULATION
Error message
HTTP/2 sockets should not be directly manipulated (e.g. read and written)
What it means
Http2Session exposes its socket through a proxy whose get trap throws ERR_HTTP2_NO_SOCKET_MANIPULATION for destroy, emit, end, pause, read, resume, write, setEncoding, setKeepAlive, and setNoDelay. HTTP/2 multiplexes many streams over a single connection, so direct reads/writes or lifecycle calls on the raw socket would corrupt the binary framing for every stream; only session-level APIs are legal.
Source
Thrown at ext/node/polyfills/http2.ts:807
const proxySocketHandler = {
get(session, prop) {
switch (prop) {
case "setTimeout":
case "ref":
case "unref":
return FunctionPrototypeBind(session[prop], session);
case "destroy":
case "emit":
case "end":
case "pause":
case "read":
case "resume":
case "write":
case "setEncoding":
case "setKeepAlive":
case "setNoDelay":
throw new ERR_HTTP2_NO_SOCKET_MANIPULATION();
default: {
const socket = session[kSocket];
if (socket === undefined) {
throw new ERR_HTTP2_SOCKET_UNBOUND();
}
const value = socket[prop];
return typeof value === "function"
? FunctionPrototypeBind(value, socket)
: value;
}
}
},
getPrototypeOf(session) {
const socket = session[kSocket];
if (socket === undefined) {
throw new ERR_HTTP2_SOCKET_UNBOUND();
}
return ReflectGetPrototypeOf(socket);View on GitHub (pinned to 9ad36f7a2c)
Solutions
- Use the session-level equivalents: session.destroy(), session.setTimeout(), session.close() instead of socket methods
- Branch on protocol before touching the socket: if (req.httpVersion === '2.0' || session.constructor.name === 'Http2Session') skip socket manipulation
- Operate on the individual Http2Stream (readable/writable sides) for data flow instead of the socket
Example fix
// before session.socket.setNoDelay(true); // throws ERR_HTTP2_NO_SOCKET_MANIPULATION session.socket.destroy(); // after session.setTimeout(5000); // allowed through the proxy session.destroy(); // session-level lifecycle API
Defensive patterns
Strategy: type-guard
Validate before calling
const isHttp2 = (s: { constructor: { name: string } }) =>
s.constructor.name === 'Http2Session';
if (!isHttp2(session)) session.socket.setNoDelay(true); // http1 only Type guard
import type { Http2Session } from 'node:http2';
import type { Socket } from 'node:net';
const isHttp2Session = (s: Http2Session | { socket: Socket }): s is Http2Session =>
(s as Http2Session).alpnProtocol !== undefined &&
typeof (s as Http2Session).goaway === 'number'; Try / catch
try {
session.socket.write(chunk);
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'ERR_HTTP2_NO_SOCKET_MANIPULATION') {
// route the data through the Http2Stream / session APIs instead
} else throw err;
} Prevention
- Never read/write the raw socket on http2 connections; use stream and session APIs
- Gate socket-tuning middleware on req.httpVersion !== '2'
- Only setTimeout/ref/unref are proxied through — design cleanup around session.destroy()
When it happens
Trigger: session.socket.write('x'); session.socket.destroy(); session.socket.pause() / resume(); session.socket.read(); session.socket.setEncoding('utf8'); session.socket.setNoDelay(true); session.socket.emit('drain'). Accessing these through the proxy (get trap, then invoking) triggers the throw.
Common situations: Middleware written for HTTP/1 that tunes sockets (setNoDelay, setKeepAlive) applied to an http2 server; code sniffing or writing raw bytes for protocol detection; test harnesses that pause/resume the socket to simulate backpressure; libraries that call socket.destroy() for cleanup on shutdown.
Related errors
- ERR_HTTP2_SOCKET_UNBOUND
- ERR_HTTP2_STATUS_INVALID
- ERR_HTTP2_SOCKET_BOUND
- ERR_HTTP2_NO_SOCKET_MANIPULATION
- ERR_FS_CP_SOCKET
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/c5291ad559e52724.
Report an issue: GitHub.