Search code examples
aws-lambdaamazon-cloudfrontaws-lambda-edge

Single Page Application with Lambda@Edge


So I have a SPA served from AWS S3 by AWS Cloudfront. I have configured the following Error Pages behaviour:

404: Not Found -> /index.html with HTTP code 200

This is needed to be able to handle routing on the client-side.

Now I've got a Lambda@Edge function which is triggered by the viewer-response event in Cloudfront and sets some custom headers like HSTS and X-Frame. The function is being invoked and works as intended on all the resources besides the actual /index.html. I'm inclined to think that this is because it's being handled by the aforementioned error pages behaviour in Cloudfront, as the actual GET request for the html is being handled by the error pages configuration in Cloudfront.

What would be a practical approach on solving this?

I'm not sure why the redirect doesn't trigger the lambda function. Is there any way to implement the same logic as the error pages configuration in lambda@edge?


Solution

  • update: The behavior of the service has changed.

    https://aws.amazon.com/about-aws/whats-new/2017/12/lambda-at-edge-now-allows-you-to-customize-error-responses-from-your-origin/

    The answer, below, was correct at the time it was posted, but is no longer applicable. Origin errors now trigger the Lambda@Edge function as expected in Origin Response triggers (but not Viewer Response triggers).

    Note, that you can generate a custom response body in an Origin Response trigger, but you don't have programmatic access to read the original response body returned from the origin, if there is one. You can replace it, or leave it as it is -- whatever it is. This is because Lambda@Edge Origin Response triggers do not wait to fire after CloudFront receives the entire response from the origin -- they appear to fire as soon as the origin finishes returning complete, valid response headers back to CloudFront.

    When you’re working with the HTTP response, note that Lambda@Edge does not expose the HTML body that is returned by the origin server to the origin-response trigger. You can generate a static content body by setting it to the desired value, or remove the body inside the function by setting the value to be empty. If you don’t update the body field in your function, the original body returned by the origin server is returned back to viewer.

    https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-updating-http-responses.html

    Important reminders: Any time you are testing changes on CloudFront, remember that your changes tend to start working sooner than you would expect -- before the distribution state changes back to Deployed, but you may need to do a cache invalidation to make your changes fully live and visible. Invalidations should include the path actually requested by the browser, not the path being requested from the origin (if different), or /* to invalidate everything. When viewing a response from CloudFront, if there is an Age: response header, you are viewing a cached response. Remember, also, that errors use a different set of timers for caching responses. These are configured separately from the TTL values in the cache behavior. See my answer to Amazon CloudFront Latency for an explanation of how to change the Error Caching Minimum TTL, which defaults to 5 minutes and does not generally respect Cache-Control headers. This is a protective measure to prevent excessive errors from reaching your origin (or triggering your Lambda functions) but is confusing during testing and troubleshooting if you aren't aware of its impact.


    (original answer follows)

    CloudFront doesn't execute Lambda functions for origin response and viewer response events if the origin returns HTTP status code 400 or higher.

    http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html

    What this means is that an unhandled error results in no response triggers firing.

    However, when an origin error is handled by a custom error response document, the origin triggers do fire on the fallback request, including Origin Response if the error document renders successfully, and here is where you'll find your solution.

    You code will run if you implement it as an Origin Response trigger instead of a Viewer Response trigger, because when fetching /index.html (the substitute error page) the origin returns 200, which invokes the Origin Response trigger -- but the Viewer Response trigger still doesn't fire. This behavior does not appear to be fully documented but testing reveals Origin Request and Response triggers firing separately on successful error document fetches, as long as the Cache Behavior with the path that matches the error document is configured with the triggers.

    In fact, it seems like an Origin Response trigger makes more sense for your application anyway, because it will be able to modify the response before it goes into the cache, and the added headers will be cached along with the response -- which should result in an overall reduction in the number of times the trigger actually needs to fire.

    You can add it as an Origin Response trigger, wait for the distribution to return to Deployed, then do a cache invalidation for /* (so that you don't serve any pages that were cached without the headers added), and after the invalidation is complete, remove the Viewer Response trigger.

    Aside: I submitted a feature request to support firing response triggers on errors but I don't know whether this is something they are considering adding, or not and apparently I wasn't the only one, since the feature was implemented and released, as described in the revised answer.