You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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.
node main.ts
perry compile main.ts -o out && ./out # default (auto-optimize) build
Expected (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,85 is function Response (req, onEnd, reject) { http.ServerResponse.call(this, req) … } with util.inherits(Response, http.ServerResponse). lib/request.js:93-94,273 does the same with Readable. 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/end and typeof req.push are undefined, and the dispatch function runs. res.writeHead(...) and res.end(...) return silently, 'finish'
never fires, and the inject() promise neither resolves nor rejects. The process exits 0 with no output.
Any util.inherits-era library that subclasses ServerResponse/IncomingMessage/Readable is 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, so util.inherits itself 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.on is a function), import { ServerResponse } from "node:http"; class X extends ServerResponse { constructor(r) { super(r) } } with setHeader/end both undefined, and util.inherits(Fn, Readable) + Readable.call(this, opts) with pushundefined (on is 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 TypeError would have surfaced as an inject() rejection, so this turns a crash into a silent hang. In a .cjs-only variant of the repro, r.setHeader('x-a','1') did throw setHeader is not a function, (a ServerResponse.call-built instance, but called inside the .cjs module), and on Object.create(ServerResponse.prototype)
in a TS file writeHead(200) threw writeHead 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 .cjs module, 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() reaches new Response, the fastify ready callback (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.ServerResponse is modeled as a
handle factory, not an initializer of this. 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 .prototype carries no methods for util.inherits/setPrototypeOf/extends to inherit. Readable.call(this) has
the same problem: stream methods are installed on the instance by the subclass-init shims, which only class extends Readable with 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).
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.prototypeandstream.Readable.prototypehave none of their methods, andServerResponse.call(this, req)/Readable.call(this, opts)install nothingon
this. Any function-style subclass (util.inherits,Object.setPrototypeOf), and even a CJSclass extends http.ServerResponse, gets instances with nosetHeader/writeHead/end(or nopush/pipe/on).new http.ServerResponse(req)itself works. ForServerResponsethis also happens with an ESMimport { 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.cjsmain.tsExpected (Node 26.5.1)
Actual (Perry, default auto-optimize build)
Note the line
res.setHeader(...) returned: calling a method whosetypeofisundefineddid not throwTypeError: res.setHeader is not a function.With
PERRY_NO_AUTO_OPTIMIZE=1the output is the same except thatnew ServerResponse(req)also has no methods.In that mode
node:httpconstructors reached throughrequireare inert anyway (the same mechanism as closed #8547),so this issue is about the auto-optimize build.
Impact
app.inject()/ light-my-request 6.6.0:lib/response.js:9-10,85isfunction Response (req, onEnd, reject) { http.ServerResponse.call(this, req) … }withutil.inherits(Response, http.ServerResponse).lib/request.js:93-94,273does the same withReadable. Checked withlight-my-request alone (
inject(dispatch, { url: '/x' })) and with fastify 5.10.0 (the audit's two other fastifyworkarounds applied): the Request and Response constructors run,
typeof res.setHeader/endandtypeof req.pushareundefined, and the dispatch function runs.res.writeHead(...)andres.end(...)return silently,'finish'never fires, and the
inject()promise neither resolves nor rejects. The process exits 0 with no output.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 streammodules written before ES classes.
Notes
util.inherits(Fn, EventEmitter)+EventEmitter.call(this)works, soutil.inheritsitself is fine. The failuresare specific to bases that Perry models natively.
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).this.setHeader('foo','bar')in light-my-request's Response constructor,
res.writeHead(...),res.end(...)) do not throw. A thrownTypeErrorwould 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, (aServerResponse.call-built instance, but called inside the.cjsmodule), and onObject.create(ServerResponse.prototype)in a TS file
writeHead(200)threwwriteHead is not a function. So far the silent return has been seen when the callis made from the TS entry module on an instance from a
.cjsmodule, and inside compiled light-my-request. Thedeciding factor is not isolated (inferred: name-based native method dispatch for http method names).
plus the audit's workarounds,
inject()reachesnew Response, the fastifyreadycallback (no error) and thedispatch, then hangs as described above. No second hang cause was found before the missing methods.
http.ServerResponseis modeled as ahandle factory, not an initializer of
this.new ServerResponse(req)reached as a value is forwarded toJS_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), whichreturns 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)hasthe 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).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.