Skip to content

fix(router): inspecting a handler ctx no longer throws on Node (#1000) - #1016

Merged
ivogt merged 2 commits into
mainfrom
fix/1000-inspect-guarded-response-headers
Oct 3, 2026
Merged

ivogt merged 2 commits into
mainfrom
fix/1000-inspect-guarded-response-headers

Conversation

@ivogt

@ivogt ivogt commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

Closes #1000.

Problem

On the Node dev server, console.log(ctx) in a handler, and util.inspect(ctx.res) or util.inspect(ctx.res.headers), throw:

TypeError: Cannot read private member #headersList from an object whose class did not declare it

The response-header guard proxies a fresh empty Headers and forwards operations to the real stub headers. Node's util.inspect unwraps that proxy and runs undici's Headers inspect hook with the proxy as this, which reads #headersList and throws. workerd does not throw, because its Headers has no Node inspect hook. The guard still has to reject header writes from a latched cache scope.

Before / after

path("/debug", (ctx) => {
  console.log(ctx);
  console.log(ctx.res.headers);
  return <p>ok</p>;
});

Before, on the Node dev server, preview, and Vercel output, that log throws and the request fails. After, it prints the stub response, including headers written on it. A ctx.res.headers.set inside a latched cache scope still throws.

End-to-end consumer example

A handler that logs the context while setting a header the page depends on:

path("/account", async (ctx) => {
  ctx.header("x-account", "ready");
  console.log(ctx.res.headers.get("x-account"));
  return <Account />;
});

Before, the console.log threw on Node before the header was useful for debugging. After, it prints ready. The same Headers object still refuses a write from inside cache() or a "use cache" body.

Semantics

Call Before After
console.log(ctx), util.inspect(ctx.res), util.inspect(ctx.res.headers) on Node throws #headersList prints the live stub headers
The same calls on workerd no throw no throw (unchanged)
ctx.res.headers.set / append / delete outside a latched cache scope writes the stub writes the stub (unchanged)
Those writes inside a latched cache scope throws the header guard throws the header guard (unchanged)
ctx.header(), setCookie(), deleteCookie() guarded as before guarded as before

Code-change summary

The inspect hook is an own property of the proxy target in packages/rangojs-router/src/server/request-context.ts. util.inspect does not consult the get trap, so a trap-only handler still throws. The hook reads rawStubHeaders on each call, because setStatus replaces that Headers. It calls undici's own hook when present, otherwise the header entries. set / append / delete still consult the write guard.

The test builds the handler context the same way cached-header-guard.test.ts already does (createRequestContext + runWithRequestContext + createHandlerContext).

Test plan

  • packages/rangojs-router/src/rsc/__tests__/cached-header-guard.test.ts — util.inspect of a handler ctx, ctx.res, and ctx.res.headers shows the live stub headers (#1000). Red first: the new test threw #headersList before the hook. Green: 28 passed in that file.
  • packages/rangojs-router/src/server/__tests__/request-context.test.ts — 41 existing tests still passed alongside the guard file (69 total across the two files).
  • pnpm run typecheck
  • pnpm run test:unit:all
  • pnpm run lint
  • pnpm run format

No e2e change. The contract is util.inspect on Node, which the unit test pins. workerd was not run; the added symbol is nodejs.util.inspect.custom, which workerd does not use.

Notes

Node 24 was not run. The assertion accepts undici's 'X-Real': 'yes' spelling from Node 22.18, the same spelling request-headers.test.ts already expects for request inspect.

ivogt added 2 commits October 1, 2026 15:03
console.log(ctx) and util.inspect(ctx.res) throw on Node because the
response-header guard proxies an empty Headers. undici's inspect hook
then reads #headersList off that proxy. The proxy target now owns an
inspect hook that formats the real stub headers.

Closes #1000.
DOM Headers has forEach and not entries, so build:types failed on the
fallback the Node inspect hook never takes.
@pkg-pr-new

pkg-pr-new Bot commented Oct 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@rangojs/router@1016

commit: ce30831

@ivogt
ivogt merged commit 5761161 into main Oct 3, 2026
43 checks passed
@ivogt
ivogt deleted the fix/1000-inspect-guarded-response-headers branch October 3, 2026 12:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

console.log(ctx) / util.inspect(ctx.res) throws 'Cannot read private member #headersList' on Node (guarded stub headers)

1 participant