Skip to content

new Response("", {status:204}) throws "Invalid response status code 204" where bun succeeds — and Response.json() doesn't enforce the same rule #10360

Description

@proggeramlug

Summary

new Response("", { status: 204 }) throws in perry and succeeds in bun:

Response constructor: Invalid response status code 204

204 is a perfectly valid status code, so the message misdescribes the cause. What perry is actually enforcing is the Fetch spec's null-body-status rule — but it enforces it against bun's behavior, and it reports it as if the code itself were out of range.

Measured (perry @ v0.5.1579 + #10356, vs bun 1.3.14)

# cell bun perry
A new Response(null, {status:204}).status 204 204 ok
B new Response("x", {status:204}).status 204 THREW Invalid response status code 204
C new Response("x", {status:205}).status 205 THREW Invalid response status code 205
D new Response("x", {status:304}).status 304 THREW Invalid response status code 304
E new Response("x", {status:201}).status 201 201 ok
F new Response("x", {status:599}).status 599 599 ok
G Response.json({a:1}, {status:204}).status 204 204 ok
7 new Response("", {status:204}).status 204 THREW Invalid response status code 204
8 new Response(undefined, {status:204}).status 204 204 ok
9 new Response(null, {status:304}).status 304 304 ok
10 new Response("", {status:200}).status 200 200 ok

So the trigger is precisely a non-null body together with a null-body status (204, 205, 304). A null or undefined body with the same status is fine, and the same body with any other status is fine.

Cell 7 is the one that matters in practice: new Response("", { status: 204 }) is how you write "no content" when your handler returns a string unconditionally, and it is extremely common in HTTP server code. OpenCode serves its own TUI backend over HTTP.

Note cell G: Response.json(..., {status:204}) does not throw, while the constructor does. Whatever check the constructor applies, the static helper does not — so the two paths already disagree with each other inside perry.

Reproducer

// @ts-nocheck
const t = (n: string, f: () => any) => {
  try { const v = f(); console.log(n, v === undefined ? "undefined" : JSON.stringify(v)) }
  catch (e: any) { console.log(n, "THREW " + e.message) }
}
t("A Response(null,204)",      () => new Response(null, { status: 204 }).status)
t("B Response('x',204)",       () => new Response("x", { status: 204 }).status)
t("C Response('x',205)",       () => new Response("x", { status: 205 }).status)
t("D Response('x',304)",       () => new Response("x", { status: 304 }).status)
t("E Response('x',201)",       () => new Response("x", { status: 201 }).status)
t("G Response.json(x,204)",    () => Response.json({a:1}, { status: 204 }).status)
t("7 Response('',204)",        () => new Response("", { status: 204 }).status)
t("8 Response(undefined,204)", () => new Response(undefined, { status: 204 }).status)

On which behavior is correct

The Fetch spec does say to throw a TypeError when a null-body status is paired with a non-null body, so perry's intent is defensible and bun is the lenient one here. Two things are wrong regardless of which side you pick:

  1. The message is wrong about the cause. "Invalid response status code 204" says the code is invalid. It is not — cell A proves perry itself accepts 204. The message should name the body/null-body-status conflict.
  2. perry contradicts itself. Response.json(..., {status:204}) builds a response with a JSON body and a null-body status without complaint (cell G), so the rule is enforced on one construction path and not the other.

For the OpenCode compatibility goal the target is bun's behavior, but even taking the strict reading, cells G and 7 need to agree with each other.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions