Networking & Content Delivery

Generating dynamic error responses in Amazon CloudFront with Lambda@Edge

Amazon CloudFront allows you to create custom error pages for specific HTTP status codes and to change response codes. CloudFront also offers origin failover capability, with which you can easily set up failover logic between combinations of AWS origins or non-AWS custom HTTP origins. This creates minimal interruption in your viewer’s experience.

However, while these CloudFront features meet the needs of most error-handling use cases, you may still want more controls on the error responses.

For example, you may want to differentiate between when the CloudFront edge location or the origin server generates an error. An edge location use case may be because an AWS Web Application Firewall (AWS WAF) rule, signed cookie or URL, or geo restriction controls the content. You may want to let the users know they’re not allowed access. If the origin generates the error instead, you may want to display a specific page with the reasons or instructions on how to request elevated access.

You can also host multiple websites on a single CloudFront distribution, forward host headers, and use virtual hosting at the origin to serve those websites. In these cases, you may want to serve different error pages depending on the website, with each identified by the CNAME (domain name) used in the request.

This post illustrates how to use AWS Lambda@Edge to create custom 403 Access Denied error pages based on:

  • Which location generates the error (CloudFront edge location or origin server)
  • The CNAME used in the request

You can easily extend this to other 4xx or 5xx response codes.

Lambda@Edge

Lambda@Edge allows you to execute an AWS Lambda function at a CloudFront edge location. This capability enables intelligent processing of HTTP requests at locations that are close to your customers for latency. To get started, upload your code (a Lambda function written in Node.js) and select one of the CloudFront behaviors associated with your distribution.

You can run a Lambda@Edge function in response to four different CloudFront events:

  • Viewer request: When CloudFront receives a request from a viewer.
  • Origin request: Before CloudFront forwards a request to the origin.
  • Viewer response: Before CloudFront returns the response to the viewer.
  • Origin response: When CloudFront receives a response from the origin.

Before continuing, make sure that you understand the basic concepts of Lambda@Edge. See Customizing Content at the Edge with Lambda@Edge for more information. For a step-by-step guide on how to create a Lambda function and associate it to a CloudFront behavior, see Tutorial: Creating a Simple Lambda@Edge Function.

Serving a custom error page on the location of the error

A common use case is to return a 403 error with a custom message if the edge location generates the error. Return a 200 OK or a 302 Redirect with a custom message if the origin server generates the error.

If AWS WAF rules, signed cookies or URLs, or geo restrictions block a request, the origin server doesn’t receive a forwarded response. Instead, the viewer receives the response directly. For cases in which the edge location generates the error, you can configure a custom 403 error using the native custom error page feature.

Request from an IP address that the AWS WAF rule blocked

Use the following command to generate an error response for a request from an IP address that the AWS WAF rule blocks:

$ curl -v http://d2fyouwzorwsid.cloudfront.net/file.txt >/dev/null
>...
< HTTP/1.1 403 Forbidden
< Content-Type: text/html
< Content-Length: 278
< Connection: keep-alive
< Date: Thu, 02 May 2019 16:28:28 GMT
< Last-Modified: Thu, 02 May 2019 15:52:07 GMT
< ETag: "caeb37d2a7d9e2d1b91b942e95d96fa6"
< Accept-Ranges: bytes
< Server: AmazonS3
< X-Cache: Error from cloudfront
< Via: 1.1 6acd4ebf1a0179dd8e00eb58764e453a.cloudfront.net (CloudFront)
< X-Amz-Cf-Id: 4D8fdE4POXbOmkwjGa9TMOMiHaYyNQwykp62SUQUM5m7QfawFWz0ag==
<
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Access Denied by the WAF Rule</title>
</head>
<body>
<h1>Access Denied by the WAF Rule</h1>
</body>
* Connection #0 to host d2fyouwzorwsid.cloudfront.net left intact

When you configure custom error pages, CloudFront serves them to the user anytime that the edge location receives an HTTP status code that matches the error code. It does not differentiate errors from the edge location from those from the origin.

Lambda@Edge allows you to customize error responses received from the origin and update the error status code to 200 OK (or to a 302 Redirect) with a custom error page. You can use the following Node.js code example in the main index.js file:

'use strict';

exports.handler = (event, context, callback) => {
    const response = event.Records[0].cf.response;
        
    const content = `
        <\!DOCTYPE html>
        <html lang="en">
          <head>
            <meta charset="utf-8">
            <title>Access Denied by the Origin Server</title>
          </head>
          <body>
            <h1>Access Denied by the Origin Server</h1>
          </body>
        </html>
        `;

        /*
         * This function updates the HTTP status code in the response to 200 OK (or 302 Found).
         * Note the following:
         * 1. The function is triggered in an origin response
         * 2. The response status from the origin server is a 403 status code
         */

    if (response.status == 403) {

    /* Generate HTTP OK response using 200 status code with HTML body. */
    
        //Response Status Code and Description
        response.status = 200;
        response.statusDescription = 'OK';
        
        //Headers
        response.headers['content-type'] = [{key: 'Content-Type', value: "text/html"}];
        response.headers['content-encoding'] = [{key: 'Content-Encoding', value: "UTF-8"}];
        
        //Response Body
        response.body = content;

        
    /* If you prefer to redirect to a custom error page */
    
      /*
        //Response Status Code and Description
        response.status = 302;
        response.statusDescription = 'Found';
        
        // Drop the body, as it is not required for redirects 
        response.body = '';
        
        //Set the Redirect Location
        response.headers['location'] = [{ key: 'Location', value: '/errors/origin-403.html' }];
      */
        
    }

        callback(null, response);
};

Associate the preceding Node.js code to a CloudFront origin response event that has an origin as an S3 bucket.

Request from an IP address that the AWS WAF rule allows, but the origin denies

Use the following command to generate an error response for a request from an IP address that the AWS WAF rule allows, but the origin denies.

$ curl -v http://d2fyouwzorwsid.cloudfront.net/file.txt >/dev/null
>...
< HTTP/1.1 200 OK
< Content-Type: text/html
< Content-Length: 297
< Connection: keep-alive
< Date: Thu, 02 May 2019 16:27:48 GMT
< Server: AmazonS3
< Content-Encoding: UTF-8
< X-Cache: Error from cloudfront
< Via: 1.1 afc572036b3eaaed6ca691594b6f9ed9.cloudfront.net (CloudFront)
< X-Amz-Cf-Id: w_fKlnCYC0pZms3-ZQ_f7D37-hPJoLJbhrfezIMRRjDxTkrLrUzcYQ==
<
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Access Denied by the Origin Server</title>
</head>
<body>
<h1>Access Denied by the Origin Server</h1>
</body>
</html>
* Connection #0 to host d2fyouwzorwsid.cloudfront.net left intact

Customizing the 403 error response based on the domain name

CloudFront lets you use your personalized CNAME for links to your files instead of using the domain name that CloudFront assigns by default.

If you host multiple websites on a single distribution, you may want to generate custom error pages based on the CNAME used in the request. For the origin server to know the domain name, the CloudFront behavior must forward the host header. The origin response event gets this host header in the request and serves an error page based on its value. You can use the following Node.js code in the main index.js file:

'use strict';

exports.handler = (event, context, callback) => {
    const response = event.Records[0].cf.response;
    const request = event.Records[0].cf.request;
    
    const hostHeader = request.headers['host'][0].value;
    const cname1 = 'domain1.example.com';
    const cname2 = 'domain2.example.com';
    
    // Default Custom error page
    const body = `
        <\!DOCTYPE html>
        <html lang="en">
          <head>
            <meta charset="utf-8">
            <title>Access Denied by the Origin Server</title>
          </head>
          <body>
            <h1>Access Denied by the Origin Server - Default Custom error page</h1>
          </body>
        </html>
        `;

    // Custom error page for the CNAME1
    const body1 = `
        <\!DOCTYPE html>
        <html lang="en">
          <head>
            <meta charset="utf-8">
            <title>Access Denied by the Origin Server</title>
          </head>
          <body>
            <h1>Access Denied by the Origin Server - Custom error page for the CNAME: domain1.example.com</h1>
          </body>
        </html>
        `;
        
    // Custom error page for the CNAME2
    const body2 = `
        <\!DOCTYPE html>
        <html lang="en">
          <head>
            <meta charset="utf-8">
            <title>Access Denied by the Origin Server</title>
          </head>
          <body>
            <h1>Access Denied by the Origin Server - Custom error page for the CNAME: domain2.example.com</h1>
          </body>
        </html>
        `;

        /*
         * This function updates the HTTP status code in the response to 200 OK.
         * Note the following:
         * 1. The function is triggered in an origin response
         * 2. The response status from the origin server is a 403 status code
         */

    if (response.status == 403) {

    /* Generate HTTP OK response using 200 status code with HTML body.*/
    
        //Response Status Code and Description
        response.status = 200;
        response.statusDescription = 'OK';
        
        //Headers
        response.headers['content-type'] = [{key: 'Content-Type', value: "text/html"}];
        response.headers['content-encoding'] = [{key: 'Content-Encoding', value: "UTF-8"}];
        
        //Response Body
        if (hostHeader == cname1) response.body = body1;
        else if (hostHeader == cname2) response.body = body2;
        else  response.body = body;
    }

        callback(null, response);
};

Conclusion

CloudFront provides the ability to create custom error pages for specific HTTP status codes and to change response codes. The origin failover capability allows you to easily set up failover logic between combinations of AWS origins or non-AWS custom HTTP origins.

The preceding solution to customizing error responses represents one example of a variety of possible use cases in which you can take advantage of Lambda@Edge’s customization power.

To learn more about the services used in this example, see Getting Started with Amazon CloudFront and Using AWS Lambda with CloudFront Lambda@Edge, and get started with our services today!