[PR #661] [MERGED] fix: defer clearRequestContext() until HTML stream is fully consumed (#660) #753

Closed
opened 2026-05-06 13:09:56 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/661
Author: @james-elicx
Created: 3/22/2026
Status: Merged
Merged: 3/23/2026
Merged by: @james-elicx

Base: mainHead: fix/660-defer-clear-request-context


📝 Commits (2)

  • 5f5ac28 fix: defer clearRequestContext() until HTML stream is fully consumed
  • 352ed07 fix: guard against stream-cancelled crashes in tick-buffered transform and early-disconnect cleanup

📊 Changes

6 files changed (+208 additions, -15 deletions)

View changed files

📝 packages/vinext/src/server/app-page-render.ts (+14 -4)
📝 packages/vinext/src/server/app-page-stream.ts (+58 -2)
📝 packages/vinext/src/server/app-ssr-stream.ts (+11 -5)
📝 tests/app-page-boundary-render.test.ts (+2 -2)
📝 tests/app-page-render.test.ts (+80 -0)
📝 tests/app-page-stream.test.ts (+43 -2)

📄 Description

Summary

  • Fixes headers()/cookies() throwing "can only be called from a Server Component" on warm (module-cached) requests in App Router pages.
  • The root cause: clearRequestContext() was called synchronously after receiving the RSC/SSR stream handle, before any Server Components had executed. The stream is lazy — components run while the HTTP layer pulls the response body.
  • Fix: pipe the HTML stream through a TransformStream whose flush() defers clearRequestContext() until the last byte is consumed. Values that must be read now (getDraftModeCookieHeader, consumeDynamicUsage, getRequestCacheLife) are still captured eagerly.

Affected files

  • packages/vinext/src/server/app-page-render.tsrenderAppPageLifecycle(): covers both the plain and ISR-cacheable HTML response paths
  • packages/vinext/src/server/app-page-stream.tsrenderAppPageHtmlResponse(): same race in the legacy/direct code path

Tests

Regression tests were written first (red) to reproduce the race, then the fix was applied (green):

  • tests/app-page-render.test.ts — 2 new tests asserting clearRequestContext is not called before stream consumption, one for each HTML code path (plain and ISR-cacheable)
  • tests/app-page-stream.test.ts — 1 new test for renderAppPageHtmlResponse; existing test updated to reflect the correct deferred timing

Closes #660


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/661 **Author:** [@james-elicx](https://github.com/james-elicx) **Created:** 3/22/2026 **Status:** ✅ Merged **Merged:** 3/23/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `fix/660-defer-clear-request-context` --- ### 📝 Commits (2) - [`5f5ac28`](https://github.com/cloudflare/vinext/commit/5f5ac2816bdd78481e32e8b3885c20086578f3fa) fix: defer clearRequestContext() until HTML stream is fully consumed - [`352ed07`](https://github.com/cloudflare/vinext/commit/352ed0797d98710117e7dcc095fb8e03d37b5b3b) fix: guard against stream-cancelled crashes in tick-buffered transform and early-disconnect cleanup ### 📊 Changes **6 files changed** (+208 additions, -15 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/server/app-page-render.ts` (+14 -4) 📝 `packages/vinext/src/server/app-page-stream.ts` (+58 -2) 📝 `packages/vinext/src/server/app-ssr-stream.ts` (+11 -5) 📝 `tests/app-page-boundary-render.test.ts` (+2 -2) 📝 `tests/app-page-render.test.ts` (+80 -0) 📝 `tests/app-page-stream.test.ts` (+43 -2) </details> ### 📄 Description ## Summary - Fixes `headers()`/`cookies()` throwing "can only be called from a Server Component" on warm (module-cached) requests in App Router pages. - The root cause: `clearRequestContext()` was called synchronously after receiving the RSC/SSR stream *handle*, before any Server Components had executed. The stream is lazy — components run while the HTTP layer pulls the response body. - Fix: pipe the HTML stream through a `TransformStream` whose `flush()` defers `clearRequestContext()` until the last byte is consumed. Values that must be read now (`getDraftModeCookieHeader`, `consumeDynamicUsage`, `getRequestCacheLife`) are still captured eagerly. ## Affected files - `packages/vinext/src/server/app-page-render.ts` — `renderAppPageLifecycle()`: covers both the plain and ISR-cacheable HTML response paths - `packages/vinext/src/server/app-page-stream.ts` — `renderAppPageHtmlResponse()`: same race in the legacy/direct code path ## Tests Regression tests were written **first** (red) to reproduce the race, then the fix was applied (green): - `tests/app-page-render.test.ts` — 2 new tests asserting `clearRequestContext` is not called before stream consumption, one for each HTML code path (plain and ISR-cacheable) - `tests/app-page-stream.test.ts` — 1 new test for `renderAppPageHtmlResponse`; existing test updated to reflect the correct deferred timing Closes #660 --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:09:56 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#753
No description provided.