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>