Description
When etags are enabled in Next.js, conditional GET requests to an OpenNext lambda with an etag matching the page generated/cached by the server and considered fresh seem to result in a status 200 response with an empty body rather than the expected 304.
As far as I can tell, the key interaction is with this behaviour in the Next server when sending the 304 response:
function sendEtagResponse(req, res, etag) {
...
if (fresh(req.headers, { etag })) {
res.statusCode = 304
res.end()
return true
}
...
}
and this block which captures the status code before the response is sent, and restores it afterwards.
const originalStatus = res.statusCode
...
if (!res.sent) {
await this.sendRenderResult(req, res, {...}) // calls through to sendEtagResponse
res.statusCode = originalStatus
}
The commit that added this (vercel/next.js@74153e1) is explicitly aiming to make sure that the 304 status code is hidden from the rest of the server so that it doesn't itself get cached.
However the response object supplied by OpenNext is not behaving the same way as a normal node one - when res.end()
is called on a ServerResponse
, the current value of statusCode
is committed immediately, while with the OpenNextNodeResponse
flushHeaders
is only called once the stream "finish"
event has been emitted asynchronously, by which time the status code has been reset back to 200 (or whatever other value it was previously). The status then returned to the lambda caller is incorrectly a 200 with an empty body, which may then get cached by CloudFront or similar, exacerbating the issue.
Not entirely sure what the right fix is here - the Next behaviour is a bit suspect, but does ultimately work with their Node server, and presumably it is necessary to allow the status code to be reset from 304 on the response to make sure it doesn't itself get cached. Maybe the OpenNextNodeResponse
can internally snapshot statusCode
at the time the response should be committed and use that for the lambda status, but still allow it to be mutated?
Reproduction
- Deploy a Next.js app router application with
generateEtags: true
config and a SSG route to lambda. - Make a request to that route to get an etag header value. The body should be returned as expected.
- Make a second request with
if-none-match
set to that etag value. This request will return 200 with no body.
In case its relevant, I've been testing with the aws-lambda-streaming
wrapper enabled and the nodejs20.x
lambda runtime.