Repository navigation
http.ServerResponse and stream.Readable have empty prototypes: subclasses built with util.inherits + Base.call(this), setPrototypeOf or class extends ServerResponse have no setHeader/end/push (light-my-request / fastify inject() never settles) #10454
Description
Found by the package audit (compiling real npm packages from source instead of Perry's native bindings) on
Perry e6dcb62 (v0.5.1587), Linux x64. http.ServerResponse.prototype and stream.Readable.prototype have none of their methods, and ServerResponse.call(this, req) / Readable.call(this, opts) install nothing
on this. Any function-style subclass (util.inherits, Object.setPrototypeOf), and even a CJS
class extends http.ServerResponse, gets instances with no setHeader/writeHead/end (or no push/pipe/on).
new http.ServerResponse(req) itself works. For ServerResponse this also happens with an ESM
import { ServerResponse } from "node:http"; class X extends ServerResponse. Calls to the missing methods also return silently instead of throwing,
so light-my-request's inject() hangs instead of failing.
Reproduction
package.json
{"name":"str6","private":true,"type":"module"}lib.cjs
'use strict';
const http = require('node:http');
const util = require('node:util');
const { EventEmitter } = require('node:events');
const { Readable } = require('node:stream');
// light-my-request's shape
function Response(req) { http.ServerResponse.call(this, req); }
util.inherits(Response, http.ServerResponse);
function SetProto(req) { http.ServerResponse.call(this, req); }
Object.setPrototypeOf(SetProto.prototype, http.ServerResponse.prototype);
class Sub extends http.ServerResponse {}
function Emitter() { EventEmitter.call(this); }
util.inherits(Emitter, EventEmitter);
function Request(opts) { Readable.call(this, opts); }
util.inherits(Request, Readable);
module.exports = { http, Response, SetProto, Sub, Emitter, Request };main.ts
import { http, Response, SetProto, Sub, Emitter, Request } from "./lib.cjs";
const req = { method: "GET", httpVersionMajor: 1, httpVersionMinor: 1, headers: {} };
const H: any = http;
const show = (label: string, o: any) =>
console.log(label, "setHeader:", typeof o.setHeader, "writeHead:", typeof o.writeHead, "end:", typeof o.end);
console.log("ServerResponse.prototype.setHeader:", typeof H.ServerResponse.prototype.setHeader);
show("new ServerResponse(req) ", new H.ServerResponse(req));
show("util.inherits + .call(this) ", new (Response as any)(req));
show("setPrototypeOf + .call(this) ", new (SetProto as any)(req));
show("class Sub extends ServerResponse", new (Sub as any)(req));
const res: any = new (Response as any)(req);
try { res.setHeader("x-a", "1"); console.log("res.setHeader(...) returned; getHeader:", typeof res.getHeader); } catch (e: any) { console.log("res.setHeader(...) threw:", e.message); }
const e: any = new (Emitter as any)();
let n = 0; e.on("x", () => n++); e.emit("x");
console.log("util.inherits(Fn, EventEmitter): on:", typeof e.on, "emitted:", n);
const r: any = new (Request as any)({ read() {} });
console.log("util.inherits(Fn, Readable): push:", typeof r.push, "pipe:", typeof r.pipe, "on:", typeof r.on);node main.ts
perry compile main.ts -o out && ./out # default (auto-optimize) buildExpected (Node 26.5.1)
ServerResponse.prototype.setHeader: function
new ServerResponse(req) setHeader: function writeHead: function end: function
util.inherits + .call(this) setHeader: function writeHead: function end: function
setPrototypeOf + .call(this) setHeader: function writeHead: function end: function
class Sub extends ServerResponse setHeader: function writeHead: function end: function
res.setHeader(...) returned; getHeader: function
util.inherits(Fn, EventEmitter): on: function emitted: 1
util.inherits(Fn, Readable): push: function pipe: function on: function
Actual (Perry, default auto-optimize build)
ServerResponse.prototype.setHeader: undefined
new ServerResponse(req) setHeader: function writeHead: function end: function
util.inherits + .call(this) setHeader: undefined writeHead: undefined end: undefined
setPrototypeOf + .call(this) setHeader: undefined writeHead: undefined end: undefined
class Sub extends ServerResponse setHeader: undefined writeHead: undefined end: undefined
res.setHeader(...) returned; getHeader: undefined
util.inherits(Fn, EventEmitter): on: function emitted: 1
util.inherits(Fn, Readable): push: undefined pipe: undefined on: undefined
Note the line res.setHeader(...) returned: calling a method whose typeof is undefined did not throw
TypeError: res.setHeader is not a function.
With PERRY_NO_AUTO_OPTIMIZE=1 the output is the same except that new ServerResponse(req) also has no methods.
In that mode node:http constructors reached through require are inert anyway (the same mechanism as closed #8547),
so this issue is about the auto-optimize build.
Impact
- fastify 5.10.0
app.inject()/ light-my-request 6.6.0:lib/response.js:9-10,85is
function Response (req, onEnd, reject) { http.ServerResponse.call(this, req) … }with
util.inherits(Response, http.ServerResponse).lib/request.js:93-94,273does the same withReadable. Checked with
light-my-request alone (inject(dispatch, { url: '/x' })) and with fastify 5.10.0 (the audit's two other fastify
workarounds applied): the Request and Response constructors run,typeof res.setHeader/endandtypeof req.pushare
undefined, and the dispatch function runs.res.writeHead(...)andres.end(...)return silently,'finish'
never fires, and theinject()promise neither resolves nor rejects. The process exits 0 with no output. - Any
util.inherits-era library that subclassesServerResponse/IncomingMessage/Readableis likely affected,
for example test injectors (light-my-request,@hapi/shot), mock req/res libraries and the classic stream
modules written before ES classes.
Notes
util.inherits(Fn, EventEmitter)+EventEmitter.call(this)works, soutil.inheritsitself is fine. The failures
are specific to bases that Perry models natively.- The same checks from a TS/ESM file (auto-optimize) give
typeof Readable.prototype.push === 'undefined'
(EventEmitter.prototype.onis a function),import { ServerResponse } from "node:http"; class X extends ServerResponse { constructor(r) { super(r) } }withsetHeader/endbothundefined, andutil.inherits(Fn, Readable)+
Readable.call(this, opts)withpushundefined(onis a function there, unlike in the CJS file). - Second contributing factor (observed, cause not investigated): calls to the missing methods (
this.setHeader('foo','bar')
in light-my-request's Response constructor,res.writeHead(...),res.end(...)) do not throw. A thrown
TypeErrorwould have surfaced as aninject()rejection, so this turns a crash into a silent hang. In a
.cjs-only variant of the repro,r.setHeader('x-a','1')did throwsetHeader is not a function, (a
ServerResponse.call-built instance, but called inside the.cjsmodule), and onObject.create(ServerResponse.prototype)
in a TS filewriteHead(200)threwwriteHead is not a function. So far the silent return has been seen when the call
is made from the TS entry module on an instance from a.cjsmodule, and inside compiled light-my-request. The
deciding factor is not isolated (inferred: name-based native method dispatch for http method names). - The audit note said "Response ctor marker never prints". That did not reproduce on this build: with fastify 5.10.0
plus the audit's workarounds,inject()reachesnew Response, the fastifyreadycallback (no error) and the
dispatch, then hangs as described above. No second hang cause was found before the missing methods. - Suspected cause (inferred from reading the code, not instrumented):
http.ServerResponseis modeled as a
handle factory, not an initializer ofthis.new ServerResponse(req)reached as a value is forwarded to
JS_NATIVE_HTTP_DISPATCH(crates/perry-runtime/src/object/class_registry/construct.rs:368-405) →
js_node_http_server_response_standalone_new(crates/perry-stdlib/src/common/dispatch/init.rs:281-290), which
returns a fresh native handle whose methods come from handle dispatch (crates/perry-ext-http/src/server/handle_dispatch.rs).
ServerResponse.call(this, req)therefore builds a handle and discards it, and the exported constructor's
.prototypecarries no methods forutil.inherits/setPrototypeOf/extendsto inherit.Readable.call(this)has
the same problem: stream methods are installed on the instance by the subclass-init shims, which only
class extends Readablewith a recognized heritage reaches (compare node:stream subclass overrides (_transform/_write/_read) ignored when the heritage is a local binding (const { Transform } = require('stream'),const T = Transform) #10448). - Related: node:stream subclass overrides (_transform/_write/_read) ignored when the heritage is a local binding (
const { Transform } = require('stream'),const T = Transform) #10448 andclass X extends AsyncResourcethrows "Class constructor AsyncResource cannot be invoked without 'new'" unless the heritage is a bareimport { AsyncResource }binding #10453 (the ES-class heritage forms of the same "native base" gap), closed class extending EventEmitter/Map/Set/Event loses its native base when constructed from another module (blocks the OpenCode TUI) #10300 and runtime: indirect subclass of a native base inherits nothing (class D extends B,class B extends EventEmitter) #6326. - No existing issue found (searched "util.inherits ServerResponse", "light-my-request inject", "ServerResponse setHeader undefined").
Activity
proggeramlug commentedon Sep 18, 2026
A WIP fix for this exists but does NOT work — do not assume the branch is a solution.
Branch
wip/10454-stream-http-inherits-empty-prototype@8418d3fb5c("route http.ServerResponse/stream.Readablethrough explicit-this construction", 6 files, 475 insertions) was written but never validated. On validation it
only partially improves the issue, confirmed across four independent configurations (fast-mode harness; direct
compile with an explicit coherent
-p perry-ext-httpbuild; default auto-optimize build matching this issue's ownrepro mode; and the same test on a hand-verified clean baseline):
stream.Readableviautil.inherits+.call(this)— genuinely fixed.http.ServerResponsecase is still completely broken, byte-identical to pristinemain:member-expression
.call(this),setPrototypeOf+.call(this),class Sub extends ServerResponse, aliasedheritage
.call(this), and theres.setHeader(...)/getHeaderround-trip.That is the primary case this issue is about — light-my-request/fastify's exact shape.
Diagnosis so far
An HIR trace (
--trace hir --focus Response) shows the branch's new static-recognition arm incrates/perry-hir/src/lower/expr_call/module_class_static.rsnever fires for the shape the test uses:It falls through to a generic
PropertyGet+Call. The dynamic runtime fallback(
maybe_construct_http_class_with_this) also fails to rescue it — even thoughJS_NATIVE_HTTP_DISPATCH's"ServerResponse"arm exists and works correctly for a plainnew ServerResponse(req).Why the runtime fallback doesn't activate was not root-caused. That is the open question.
Related, and worth reading first
const { Transform } = require('stream'),const T = Transform) #10448 / PR fix(runtime): dispatch node:stream super() through any bound-export heritage shape #10649 — thenode:streamsibling, validated and shipped. It fixes indirect heritage at theruntime level in
js_fetch_or_value_superby value identity rather than syntax, which is a different and morerobust angle than the static-recognition arm this branch attempts.
extendsidentifier looks "locally shadowed", whichinside a CommonJS wrapper is every
const { X } = require(...).The clone is left at
/root/claude-fix-10454on perrymaster (target/ deleted) with its OWNER file.fix(runtime): stdio writable state, ServerResponse/Readable inherits-…
fix(runtime): stdio writable state, ServerResponse/Readable inherits-…
fix(runtime): reach the stream subclass-init shim through the stream …
perf: this is a parameter of every JS body; the implicit-this cell is…
perf: function objects point to a static JsFunctionInfo; delete the c…
fix(runtime): the native-this alias lives on its object