Repository navigation
feat(rsc): add unstable_headers for setting response headers - #15729
Open
brookslybrand wants to merge 3 commits into
Open
brookslybrand wants to merge 3 commits into
brookslybrand wants to merge 3 commits into
Conversation
Server functions, loaders, actions, and middleware (before next()) can set response headers via unstable_headers(), so server functions can set cookies without redirecting. Set-Cookie headers are appended; other headers replace values from route headers exports or redirect responses. It throws once the response has been created, including while server components render.
Contributor
Preview Build AvailablePreview builds have been created for this PR. You can install pnpm install "remix-run/react-router#preview/pr-15729&path:packages/react-router"And/or install other packages via: pnpm install "remix-run/react-router#preview/pr-15729&path:packages/react-router-dev"
pnpm install "remix-run/react-router#preview/pr-15729&path:packages/react-router-express"
pnpm install "remix-run/react-router#preview/pr-15729&path:packages/react-router-node"
pnpm install "remix-run/react-router#preview/pr-15729&path:packages/react-router-serve"These preview builds will be updated automatically as you push new commits. |
- Support resource routes (and middleware above them) by running them in ServerStorage, which also makes unstable_getRequest work there - Add headers to Flight responses middleware returns without next() - Reject React Router managed headers (Content-Length, Content-Type, Location, React-Router-Resource, X-Remix-*) - Throw on writes through a saved reference after the response exists - Replay set/append/delete in order instead of always replacing - Skip exact duplicate Set-Cookie headers - Explain when another react-router copy handles the request - Stop aliasing unstable_headers as headers in docs and tests
Apply the headers once to the final response in matchRSCServerRequest instead of at every response construction point. This removes the response tracking, per-path helpers, and cookie de-duplication, since intermediate responses never get the headers. Consolidate tests.
Member
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What problem is this PR solving?
redirect()/replace(), which navigates (and resets scroll), so setting a cookie from a server function needs a redirect or app-level plumbing.unstable_headers(), which returns the outgoing responseHeadersfor the current request. It works in server functions, loaders, actions, resource routes, and middleware (beforenext()), in RSC Data and RSC Framework Mode.next()(Flight or not).set,append, anddeletecalls are replayed in order on the final response, on top of its other headers (routeheadersexports, returned/thrown responses, and anything middleware set afternext()).Set-Cookieis always appended.Content-Type,Content-Length,Location,React-Router-Resource,X-Remix-*) can't be set, since overriding them breaks the protocol (e.g. a staleContent-Lengthtruncates the HTML document).new Response()).Context: reactrouter.com's RSC migration (remix-run/react-router-website#258) sets its color scheme cookie with an
AsyncLocalStorageslot in root middleware to avoid a redirect. This replaces that with one call. Next.js covers the same need withcookies().set()in server functions.Implementation
ServerContext.headersis aHeaderssubclass that guards writes (reserved names, after the response exists) and records non-cookie operations so they can be replayed with their realset/append/deletemeaning.matchRSCServerRequestcreates the context and applies the headers once to the final response, for both render and resource routes. Intermediate responses (e.g. fromnext()) never get them, so nothing is applied twice.generateRenderResponselocks the headers when it callsgenerateResponse, before Flight renders any server component.ServerStoragetoo, so they (and middleware above them) can useunstable_headers(), andunstable_getRequest()works there as well.Open questions
@jacob-ebey, thanks for the implementation guide. A few calls I'd like your take on:
unstable_headers()per your suggestion. In Next.js,headers()is the read-only request headers (cookies()is the one that writes), sounstable_headers().get("Cookie")silently returningnullcould trip people up. It also collides with the route moduleheadersexport if someone imports itas headers, so the docs useunstable_headersdirectly. Alternatives:unstable_responseHeaders(), or acookies()-style API. Thoughts?headersexports, redirect responses, and headers middleware set afternext(), so an explicit call is never silently dropped andappend("Vary", …)really appends. The other option is to seed them as defaults (like side-effect redirect headers today), which routeheadersfunctions can override. Which do you prefer?await. SincegenerateResponseconstructs theResponsebefore Flight runs any component, render-time calls always throw here, with an error pointing to loaders, actions, server functions, and middleware. Supporting render would mean deferring response construction. Is throwing OK for now?unstable_headers()in root middleware made every resource route 500, so they're now supported via their ownServerStorage.run. That also makesunstable_getRequest()work in resource routes. OK with that?status(). Left out to keep this focused. Mostly useful for no-JS form posts. Happy to do it as a follow-up.🎩 Tophatting
Unit tests in
packages/react-router/__tests__/rsc/headers-test.tsand an integration test inintegration/rsc/rsc-test.ts("Supports unstable_headers in loaders and server functions") cover:Set-Cookiewithout redirecting → cookie on the response; with JS,document.cookieupdatesheaders()value wins over the redirect'sheaderssetsVary/Cache-Control/X-Remove, loader appends/sets/deletes →Accept-Encoding, Cookie,no-store, removedContent-Length,Content-Type,Location,React-Router-Resource, orX-Remix-*→ throwsunstable_headers(), or a deferred promise writes to a saved reference → throws "headers can't be used after the response has been created"Generated by an AI agent on behalf of Brooks Lybrand. Model: Claude Opus 5.5; reasoning: unknown.