For compressed responses, omit Content-Length
and Content-Encoding
headers #23
Description
The fetch
API is becoming the standard HTTP client for server usage. When proxying back a fetched resource to the user-agent, here's what i'd intuitively do:
async function myEndpoint(request: Request): Promise<Response> {
return await fetch("https://www.gatsbyjs.com/Gatsby-Logo.svg");
}
The URL in this example responds with a gzip-compressed SVG. As specced in 16.1.1.1 of 15.6 HTTP - network fetch
, the body
stream contains an uncompressed version of the SVG, making compression transparent to the user. This works well for consuming the SVG.
However, the spec does not require headers to mirror this decompression. Although the Response
has uncompressed body, the Content-Length
headers shows it compressed length and the Content-Encoding
header pretends it's compressed. In nodejs/undici#2514 (the issue I originally opened), I've included a repro of what this means if the Response
is proxied to the user-agent:
- The
body
ends up being longer than the headers described. This is not allowed in the HTTP spec, and leads clients like cURL to warn. - The user-agent will try decompress a body that isn't compressed
The current workaround is to manually alter headers on responses from fetch
:
async function myEndpoint(request: Request): Promise<Response> {
let resp = await fetch("https://www.gatsbyjs.com/Gatsby-Logo.svg");
if (resp.headers.get("content-encoding") {
const headers = new Headers(resp.headers)
headers.delete("content-encoding")
headers.delete("content-length")
resp = new Response(body, { ...resp, headers })
}
return resp
}
This is cumbersome and should live in library code. It can't live in frameworks, because it's impossible to differentiate a response produced by fetch (content-encoding header, but uncompressed body) from a compressed Response created in user code (same content-encoding header, but compressed body).
Instead of this workaround, fetch
should require the content-length
and content-encoding
headers to be deleted when response body is decompressed.
Some implementors already do this, including Deno and workerd. Netlify might implement the same for Netlify Functions 2.0.
I've checked undici (Node.js), node-fetch, Chrome and Safari - all of them expose the behaviour explained above, where the headers don't match the body.
An alternative solution for this would be whatwg#1524 - that way, frameworks can use the decompressed body field to tell compressed responses from uncompressed ones.
Summary:
What's the problem? compressed responses having content-encoding
header, but decompressed body
leads to incoherent Response
What's the usecase? mostly reverse proxying
Is it relevant to upstream? Yes, potentially because of service workers.
What's my suggestion for a fix? Upon decompression, content-length
and content-encoding
headers should be deleted.
How do engines deal with this? Most have the bug, but Deno and workerd implemented the fix I propose.