Skip to content

fix(hir): #1070 — instanceof narrowing breaks property read in fastify error handler - #1077

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-agent-abff0ac1226002000
May 19, 2026
Merged

fix(hir): #1070 — instanceof narrowing breaks property read in fastify error handler#1077
proggeramlug merged 1 commit into
mainfrom
worktree-agent-abff0ac1226002000

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Closes #1070.

Root cause

pre_scan_fastify_handler_params (in crates/perry-hir/src/lower_patterns.rs) registered the first arrow param of every recognized fastify handler call as a ("fastify", "Request") native instance — including app.setErrorHandler(async (err, req, reply) => …), whose first param is the thrown value, not a Request.

With err tagged as Request, any user-field read like err.problem lowered to a NativeMethodCall { module: "fastify", method: "problem", … }. problem isn't in the Request method dispatch table, so the lower_native_method_call fall-through (crates/perry-codegen/src/lower_call/native.rs:2297) returned double_literal(0.0) — the access printed as 0.

(err as MyError).problem worked because expr_member.rs's native-instance shape check only matches member.obj == Expr::Ident; a TsAs cast bypassed it and went through generic property dispatch — which is why the bug report's outside-the-if access printed the full object but the inside-the-narrow bare err.problem access printed 0.

Fix

Skip the err param for setErrorHandler so only params[1]=request and params[2]=reply get the native-instance tags. err.<field> then falls through to the generic property dispatch that reads the actual class-instance field at runtime.

Verification

Reproducer from the issue:

[A] outside if — problem: {"title":"Bad Request","status":400}
[A] inside if — problem: {"title":"Bad Request","status":400}   ← pre-fix: 0
[A] const p: {"title":"Bad Request","status":400}                ← pre-fix: 0
[B] no-narrow as-cast — problem: {"title":"Bad Request","status":400}

Regression coverage added in test_fastify_integration.ts + run_fastify_tests.sh:

  • POST /throw-problem throws a ProblemError instance from a route
  • setErrorHandler narrows via instanceof and reads err.problem.{status,title}
  • HTTP 400 with {"title":"Bad Request","status":400} body

All 12 fastify integration tests pass (10 pre-existing + 2 new for #1070).

No version bump or changelog change — maintainer handles that at merge time.

Test plan

…uest

`pre_scan_fastify_handler_params` in `crates/perry-hir/src/lower_patterns.rs`
registered the first arrow param of EVERY recognized fastify handler call
as a `("fastify", "Request")` native instance — including
`setErrorHandler(async (err, req, reply) => …)`, whose first param is the
thrown value, not a Request. With `err` tagged as Request, any user-field
read like `err.problem` lowered to a `NativeMethodCall { module: "fastify",
method: "problem", … }` whose unknown-method fall-through in
`lower_native_method_call` returns `double_literal(0.0)`, so the access
printed as `0`.

`(err as MyError).problem` worked because `expr_member.rs`'s native-instance
shape check only fires for `member.obj == Expr::Ident`; a `TsAs` cast
bypassed it and went through generic property dispatch.

Skip the err param for `setErrorHandler` so only `params[1]=request` and
`params[2]=reply` get the native-instance tags. `err.<field>` then falls
through to the generic property dispatch that reads the actual class-
instance field at runtime, restoring parity with the `as`-cast path.

Regression coverage added to `test_fastify_integration.ts` /
`run_fastify_tests.sh`: a `ProblemError` thrown from a route is
caught by `setErrorHandler`, narrowed via `instanceof`, and its
`err.problem.{status,title}` fields read correctly into the
HTTP 400 response body.
@proggeramlug
proggeramlug merged commit b6b790c into main May 19, 2026
9 checks passed
@proggeramlug
proggeramlug deleted the worktree-agent-abff0ac1226002000 branch May 19, 2026 07:27
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.

instanceof narrowing breaks property access in Fastify async error handler

1 participant