Automatically compress route handler responses if not already compressed (#101220)
Route handler responses were never compressed when self-hosted.
`NodeNextResponse.appendHeader` stored every header value as an array,
even a single value. The compression middleware checks `Content-Type`
with the `compressible` package, which only accepts strings, so any
response sent through `sendResponse` (all app route handlers, including
prerendered ones served from the cache) failed the content-type filter
and skipped compression. `appendHeader` now keeps a single value as a
plain string, matching Node's own `appendHeader` semantics.
Fixes https://github.com/vercel/next.js/issues/101120
Closes https://github.com/vercel/next.js/pull/101201
We must strip CE and CL headers from `Response` returned from route
handlers due to
https://github.com/vercel/next.js/security/advisories/GHSA-pc58-hwjq-p385.
A `Response` from `fech()` is indistinguishable from a synthetic one.
Any provenance can get lost via `clone` or manual reconstruction based
on headers and body.
Instead, the responses from route handlers should be returned
uncompressed and be compressed by the outgoing server: now `next start`
or your hosting provider e.g. Vercel.
Route handlers should not return a `fetch()` without processing the body
as this needlessly adds CPU overhead due to `fetch()`'s automatic
decompression. Use a dedicated proxy library for that instead.
## test plan
- moved dev-only compression test to e2e and extended with small/large
and dynamic/static routes
<!-- NEXT_JS_LLM -->
---------
Co-authored-by: Claude Code (kimi-k3) <noreply@anthropic.com>