Skip to content

fix(response): allow status and headers staged during the first stream chunk#1512

Merged
pi0 merged 1 commit into
mainfrom
fix/stream-response-status
Jul 24, 2026
Merged

fix(response): allow status and headers staged during the first stream chunk#1512
pi0 merged 1 commit into
mainfrom
fix/stream-response-status

Conversation

@pi0x

@pi0x pi0x commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Closes #1510 (partially — see caveat).

What I found

Reproduced the report on main. iterable() (suggested in the issue thread) did not help either — two independent problems:

1. HTTPResponse silently discards event.res.status

Pre-existing and unrelated to streaming:

app.get("/", (event) => {
  event.res.status = 201;
  return html`<p>hi</p>`;   // -> 200 ❌   (returning "hi" instead -> 201 ✅)
});

prepareResponse does res.status || preparedRes?.status, but res is the HTTPResponse and its getter was this.#init?.status || 200 — never undefined, so the staged status was dead code for every util returning an HTTPResponse (html(), iterable(), user-built ones). Same for statusText.

HTTPResponse.status / .statusText now return undefined when unset, meaning "inherit": event.res.status is used, falling back to 200. prepareResponse itself is unchanged — the || preparedRes?.status fallback finally does what it was written to do.

2. iterable() built its response before the producer ran

The ReadableStream was constructed synchronously, so toResponse had already built the Response by the time pull first ran. iterable() now awaits the first chunk before constructing the HTTPResponse — the same window v1 had, where headers went out on first write:

app.get("/", (event) =>
  iterable(async function* () {
    const data = await load();
    event.res.status = 201;
    event.res.headers.set("hello", "world");
    yield render(data);
  }),
);

Side effect: a throw before the first chunk now renders a proper error response instead of tearing a stream mid-flight.

Caveat

Returning a raw ReadableStream (the exact snippet in #1510) still cannot support this — the Response must be constructed before anything reads the stream, and eagerly priming every returned stream would stall headers for SSE/long-poll bodies. iterable() is the supported way to keep a v1-style window; documented on the util.

Breaking

  • HTTPResponse.status is number | undefined (was number, always ≥ 200); statusText likewise.
  • iterable() returns Promise<HTTPResponse> (as does the deprecated sendIterable). Returning it from a handler is unaffected — toResponse already awaits.

Size

Bundle budgets unchanged and still green — this comes out ~11 bytes smaller than main (dropping the || 200 / || "OK" defaults pays for the fix).

Tests added in test/status.test.ts (staged status vs. own status) and test/iterable.test.ts (status/headers before, during and after the first chunk; throw before the first chunk). Full suite + lint green.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved streaming responses so status codes and headers set while generating the first chunk are applied correctly.
    • Explicit response status and status text now take precedence over staged values.
  • Bug Fixes

    • Corrected error handling when the first streamed chunk fails.
    • Unset response status and status text now remain unset instead of defaulting automatically.
  • Documentation

    • Clarified when streaming response headers are finalized.

…m chunk

Two related regressions surfaced by #1510:

- `HTTPResponse.status` / `.statusText` defaulted to `200` / `"OK"`, so
  `res.status || preparedRes?.status` in `prepareResponse` always picked `200`
  and silently discarded a status staged on `event.res` by the handler or a
  middleware. Any util returning an `HTTPResponse` (`html()`, `iterable()`, ...)
  was affected. They now return `undefined` when unset, meaning "inherit".

- `iterable()` built its `ReadableStream` synchronously, so the response was
  created before the producer ran. Pull the first chunk before constructing the
  `HTTPResponse` — the same window h3 v1 had — so status and headers set while
  producing it are still applied. `iterable()` now returns a promise.

Co-Authored-By: Claude Opus 5 <[email protected]>
@pi0x
pi0x requested a review from pi0 as a code owner July 24, 2026 18:13
@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
h3 Ready Ready Preview, Comment Jul 24, 2026 6:14pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

iterable() now awaits the first stream chunk before creating an HTTPResponse. Response status and headers staged during first-chunk production are applied, later mutations are ignored, and unset HTTPResponse status fields inherit staged values.

Changes

Iterable response timing

Layer / File(s) Summary
Response status inheritance
src/response.ts
HTTPResponse.status and statusText return undefined when not explicitly set.
First-chunk response creation
src/utils/response.ts, src/_deprecated.ts, docs/2.utils/2.response.md
iterable() prefetches the first chunk before creating the response, updates stream pulling, and returns a promise through both APIs.
Status and error validation
test/iterable.test.ts, test/status.test.ts
Tests cover staged status and headers, post-first-chunk mutations, first-chunk errors, and explicit status precedence.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • h3js/h3#1511: Also defers response preparation until the first stream payload.

Suggested reviewers: pi0, pi0

Poem

A rabbit watched the first chunk flow,
Then stamped the headers down below.
Status waited, bright and clear,
Late changes vanished from the ear.
Streams now hop in proper time!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main fix to apply staged status and headers during the first stream chunk.
Linked Issues check ✅ Passed The PR matches #1510 by delaying iterable response creation until the first chunk and preserving staged status and headers.
Out of Scope Changes check ✅ Passed The changes are scoped to the stream response timing fix and its related type, behavior, docs, and tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/stream-response-status

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/status.test.ts`:
- Around line 84-97: Update the test around the “own status wins” handler to
stage a conflicting event.res.statusText value as well as event.res.status, then
assert the returned HTTPResponse’s statusText remains “teapot.” Ensure the
assertions cover both response-owned status and statusText so reversed fallback
precedence would fail.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 380312fe-cf51-4226-97d0-51d15c6d4cf0

📥 Commits

Reviewing files that changed from the base of the PR and between 321b1dc and a6681cf.

📒 Files selected for processing (6)
  • docs/2.utils/2.response.md
  • src/_deprecated.ts
  • src/response.ts
  • src/utils/response.ts
  • test/iterable.test.ts
  • test/status.test.ts

Comment thread test/status.test.ts
@pi0
pi0 merged commit 6ebd459 into main Jul 24, 2026
10 checks passed
@pi0
pi0 deleted the fix/stream-response-status branch July 24, 2026 18:18
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.

[v2] Regression: Response sended before first stream payload

2 participants