Skip to content

Conditional GET requests don't appear to be handled correctly #631

Closed
@mdlawson

Description

@mdlawson

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions