Skip to content

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

@proggeramlug

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) 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindings

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions