aws-amplify / amplify-hosting

AWS Amplify Hosting provides a Git-based workflow for deploying and hosting fullstack serverless web applications.
https://aws.amazon.com/amplify/hosting/
Apache License 2.0
449 stars 113 forks source link

502 error on Amplify SSR Functions if uncompressed response size is over 1MB #2221

Open nicksemansonos opened 3 years ago

nicksemansonos commented 3 years ago

Before opening, please confirm:

App Id

d2icpnoec5rp5b

Region

us-east-1

Amplify Console feature

Not applicable

Describe the bug

We noticed that one of our SSR API routes (defined in the /pages/api directory of our Next.js app) was 502ing in production (Amplify to Lambda@Edge,) but working as expected running locally. With a bit of research and viewing the logs, we discovered that this was because the response was over CloudFront’s 1MB limit for “Size of a response that is generated by a Lambda function, including headers and body.”

ERROR Validation error: The Lambda function returned an invalid response, the length of the body size and header was 1054968 bytes which exceeded the CloudFront limit of 1048576 bytes on the maximum length of the request including body size and headers.

This was strange though, because even though the uncompressed size was 1.01MB locally, the actual response size was around 241kb. This is according to the network inspector in the browser. This difference makes sense, Next.js uses gzip internally to compress the responses to save space. If CloudFront was receiving this 241kb response from the Lambda function, it shouldn’t be failing for being too large.

Other responses from the same route (with different parameters) were coming back from Lambda@Edge with gzip compression. It would appear that CloudFront is not accepting the gzip compressed response of the Lambda function, and is either bypassing the internal Next.js gzip, or is uncompressing the response before processing it. Then, before it is sent as a final response, it is applying its own gzip compression again.

Expected behavior

Since the final compressed response was under 1MB, we expected this route to work without hitting the size limitation.

Reproduction steps

  1. Create an API route in a Next.js project that will deliver an uncompressed response size of over 1MB, but a compressed size of under 1MB.
  2. Even though the final compressed size will be under 1MB, the CloudFront error will occur, and a log message will be generated saying the response was too large.

In our case, we are processing a large number of Salesforce records and returning the result as JSON. The uncompressed response was 1.01MB but compressed it was 241KB. This caused the error, even though 241KB is well within the 1MB limit.

Build Settings

No response

Additional information

Workaround

On the problematic API route, I manually added gzip compression to the response by using the zlib library and setting the Content-Encoding header to “gzip”. Even though Next.js usually does this part on its own internally, I suspected that doing it manually would “force” CloudFront to accept the gzipped version of the response, which would be within the size limitation.

Before deploying, I tested the workaround locally and was happy to see that it worked exactly as expected. The network inspector showed the same response size and uncompressed size as before, even though now I was manually adding the gzip compression. This means that Next.js luckily didn’t “double compress” the response, which was exactly what we needed.

So it was time to deploy to Amplify. We were pleasantly surprised to find that the API route now worked perfectly! Even though the uncompressed size was still over the 1MB limit, the response being sent to CloudFront was well under the limit, and so it was processed successfully. CloudFront also must have noticed it was already compressed with gzip, so it also did not “double compress” the response. This also suggests that CloudFront wasn’t uncompressing the response from the Lambda first, because that would have happened here too.

Luckily, this is our only API route that comes close to the size limit, so we don’t need to manually re-add gzip compression to everything. That would be a pretty tedious “solution” to the issue at hand.

Conclusion

It seems like if Lambda didn’t ignore/bypass the native gzip compression of Next.js, it could process much “larger” response payloads that are still well within the 1MB limit, provided they are compressed with Next.js internal gzip before CloudFront has to handle it.

In conclusion, the 1MB Lambda@Edge response limit in a Next.js SSR Amplify project currently effectively applies to the uncompressed size, even though the actual final response size will still be compressed with gzip and might well be under the 1MB limit. At the very least, this limitation should be clarified in the Amplify, CloudFront, or Lambda@Edge documentation.

ferdingler commented 3 years ago

Hi @nicksemansonos, thank you very much for the detailed report. This is indeed interesting. Are you positive that NextJs should be gzipping the content of SSR pages? Running it locally may not be an accurate comparison with the deployed version as their local server may behave differently than a production build.

Their documentation suggests that you should enable this on your proxy to offload the load from NodeJS: https://nextjs.org/docs/api-reference/next.config.js/compression. In this case your proxy would be CloudFront but as I understand your description, the problem is that the Lambda@Edge is returning the response uncompressed which is what causes the payload limit to be exceeded.

We are going to look into this closer, but glad to hear that there is a workaround to gzip the content yourself even though is not an ideal solution. Thanks for reporting and let us know if you find additional information.

nanno77 commented 2 years ago

@ferdingler Any update on this one. We are having a very similar problem with Amplify using NextJS SSR backed by WP Graphql and certain larger pages tend to throw the 502 error described by @nicksemansonos above.

Since our pages are dynamic it is harder for us to add GZIP to a specific API endpoint. Hoping there has been some traction on this issue.

Thanks.