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

  1. added
    package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindings
    on Sep 17, 2026
  2. proggeramlug commented on Sep 18, 2026

    @proggeramlug
    ContributorAuthor

    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.Readable
    through 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-http build; default auto-optimize build matching this issue's own
    repro mode; and the same test on a hand-verified clean baseline):

    • stream.Readable via util.inherits + .call(this) — genuinely fixed.
    • Every http.ServerResponse case is still completely broken, byte-identical to pristine main:
      member-expression .call(this), setPrototypeOf + .call(this), class Sub extends ServerResponse, aliased
      heritage .call(this), and the res.setHeader(...)/getHeader round-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 in
    crates/perry-hir/src/lower/expr_call/module_class_static.rs never fires for the shape the test uses:

    const H: any = http;
    H.ServerResponse.call(this, req);

    It falls through to a generic PropertyGet + Call. The dynamic runtime fallback
    (maybe_construct_http_class_with_this) also fails to rescue it — even though JS_NATIVE_HTTP_DISPATCH's
    "ServerResponse" arm exists and works correctly for a plain new ServerResponse(req).

    Why the runtime fallback doesn't activate was not root-caused. That is the open question.

    Related, and worth reading first

    The clone is left at /root/claude-fix-10454 on perrymaster (target/ deleted) with its OWNER file.

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