Skip to content

#1240 follow-up: request.json() result is DCE'd when only used in if(!body) — needs to be 'observed' (e.g. typeof) to survive #1293

Description

@proggeramlug

Summary

The fix for #1240 makes request.json() work — but only when its
return value is observed (passed to typeof, logged, accessed via
property). When the return is only used in an if (!body) truthiness
check (the standard Fastify body-parse pattern), Perry's DCE appears
to eliminate the call and body comes through as undefined.

Found while sweeping 27 call sites of const body = request.json() as any; if (!body) return reply.status(400)… across our routes after
pulling the #1240 fix.

Repro

Two handlers, structurally identical except the second one passes the
return through typeof. The first one returns 400 (body is undefined);
the second returns 200 (body is the parsed object).

// src/main.ts
import Fastify from "fastify";

const app = Fastify({ logger: false });

// Broken: return value only used for if(!body) truthiness.
app.post("/no-anchor", async (request, reply) => {
  const body = (request as any).json() as any;
  if (!body) return reply.status(400).send({ where: "no-anchor", body: "falsy" });
  return { where: "no-anchor", body };
});

// Works: same logic but with a typeof "observation" first.
app.post("/with-anchor", async (request, reply) => {
  const j = (request as any).json();
  console.log("[with-anchor] typeof=" + typeof j);   // <-- the anchor
  const body = j as any;
  if (!body) return reply.status(400).send({ where: "with-anchor", body: "falsy" });
  return { where: "with-anchor", body };
});

// Works (this is the canonical Fastify pattern we ended up shipping):
app.post("/use-body", async (request, reply) => {
  const body = (request as any).body as any;
  if (!body) return reply.status(400).send({ where: "use-body", body: "falsy" });
  return { where: "use-body", body };
});

await app.listen({ port: 3098, host: "127.0.0.1" });

Compile + test:

perry compile src/main.ts -o dcebug
./dcebug &
for ROUTE in no-anchor with-anchor use-body; do
  echo "=== $ROUTE ==="
  curl -s -X POST "http://127.0.0.1:3098/$ROUTE" \
    -H 'Content-Type: application/json' -d '{"hello":"world"}'
  echo
done

Actual output:

=== no-anchor ===
{"where":"no-anchor","body":"falsy"}        ← 400, body undefined
=== with-anchor ===
{"where":"with-anchor","body":{"hello":"world"}}    ← 200, body OK
=== use-body ===
{"where":"use-body","body":{"hello":"world"}}        ← 200, body OK

The only difference between /no-anchor and /with-anchor is the
single console.log("typeof=" + typeof j) line. Removing it causes
request.json()'s return to be lost.

Expected

The standard Fastify body-parse pattern in user code is:

const body = request.json();
if (!body) return reply.status(400).send({ error: "Invalid body" });
const { email } = body;

This should consistently return the parsed body. Today the if (!body)
check by itself is apparently not "enough" of an observation to
prevent the codegen from eliminating the call.

Workaround we shipped

Switched all 27 call sites to (request as any).body (canonical
Fastify — .body is set by the content-type parser, which works
correctly under Perry already). This unblocked the full POST API
(login, signup, password reset, webhooks, IAP validation).

But the question for upstream: should the canonical
const body = request.json(); if (!body) … pattern work, or is
request.body the only blessed path?

Context

Found in the same investigation that produced #1240. After your fix
landed, I rebuilt locally (perry binary + perry-runtime staticlib,
both off ced5c862), confirmed the fix with a tiny smoke test that
returned typeof of the result (worked), then redeployed our api and
saw the original symptom return on the un-touched call sites. Took a
minute to realize that the smoke-test version "happened to" observe
the return via typeof and the real handlers didn't.

Compiler state at time of repro:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions