next.js
3c97df56 - Restore the live `headers()` view of the incoming request (#97166)

Commit
32 days ago
Restore the live `headers()` view of the incoming request (#97166) #94703 and #95116 both changed how internal headers are kept out of userland `headers()`, and between them they left `headers()` detached from the request it describes. #94703 started stripping the dev-only request-id headers alongside the flight headers that had been stripped for much longer. Those deletes ran against the shared request headers, because `HeadersAdapter.from` returns a `Headers` instance unchanged instead of copying it, and removing the request-id headers there broke the dev debug channel when the dev server rendered a redirect target after a server action. #95116 fixed that by copying the headers before stripping them. Neither change set out to alter whether `headers()` follows the request. The sealed view has read through to the underlying headers for as long as `HeadersAdapter.seal` has existed, and its own unit tests assert that. The copy was only a way to keep the deletes from escaping into `req.headers`, and the detached view was collateral that no test covered. The result, since 16.3.0, is that a Proxy which writes a header onto `request.headers` and then reads it back through `headers()` gets the value from before the write. Reading the same header directly off `request.headers` returns the new value, so two APIs describing the same request disagree, and a second `headers()` call returns the same stale view. Whether the write is seen at all depends on ordering, because the view is built on first access: a write made before the first `headers()` call is observed, and the identical write made after it is not. This change drops the copy and stops deleting anything. `HeadersAdapter.seal` now accepts a set of header names to omit from every read operation, and `getHeaders` seals the shared request headers directly with the flight headers and the dev request-id headers hidden. Both earlier intentions survive. The internal headers stay on the request where the framework still reads them, they stay invisible to userland `headers()`, and the sealed view tracks the request again, so the two APIs agree throughout a Proxy run. Hiding on read is slightly stricter than the delete it replaces. The delete ran once, when the view was created, so an internal header written to the request afterwards would have shown through. It now stays hidden no matter when it is written. While restructuring `seal` we also corrected the `parent` argument that `forEach` passes to its callback. The native method passes the unsealed target, which hands the callback a mutable handle on the underlying headers and defeats the seal. Both the plain and the hiding handler now pass the sealed proxy instead. `cookies()` is unaffected and stays a snapshot, because `RequestCookies` parses the `cookie` header when it is constructed. That predates 16.3 and is left alone here. The fix is based on #97145 by @tachsin. That pull request comes from a fork, and deploy tests do not run for fork pull requests, so it is recreated here on a branch of this repository to get full CI coverage. The commit carries a `Co-authored-by` trailer for them, so their credit is preserved on merge. We also took the liberty of a few improvements over the original: `seal` is restructured into two handlers rather than a guard repeated in each case, `getSetCookie` and `forEach` are handled as described above, and the unit and end-to-end coverage is wider. fixes #97049 closes #97145 --------- Co-authored-by: joaquin <tachsinachmet@gmail.com>
Author
Parents
Loading