Specifying different error behaviors for different S3 origins on CloudFront

0

Hello, I have the following situation and I need help on how to proceed:

  • I have a multi-page website in a private S3 bucket. It uses an OAI to allow a CloudFront distribution to distribute this website at example.com.

  • I have a single-page application hosted in another private S3 bucket. It uses an OAI to allow the same CloudFront distribution to distribute the website at example.com/app. The CloudFront distribution contains an "Additional Behavior" which directs requests to the path /app* to the app's S3 bucket.

My problem lies in the fact that I need different error behaviors for both origins. The first origin, the S3 bucket containing the website, returns 404.html on 404 Not Found. This is simple enough to do; I can specify the error response for the distribution to use this filepath. The problem is that the second origin, the SPA served at example.com/app, needs to return its index.html document on 404, since it is an SPA. I cannot specify error handling in CloudFront on a per-origin basis.

I have two known workarounds to this issue:

  1. Use the static website feature on S3 for the SPA and specify its error document there.
  2. Create a Lambda@Edge function to somehow customize the error handling behavior for URIs to /app*.

My understanding is that it is recommended to avoid the static website feature of S3 and instead use an OAI to CloudFront, so I'd prefer to implement option 2. I am having trouble writing the necessary function however, and I can't find any examples where someone has implemented something similar. Any guidance or help on this issue would be appreciated - either in terms of accomplishing the above or if there is a superior strategy altogether.

Cole
asked a month ago349 views
2 Answers
1

You can create a Lambda@Edge function that triggers on the "Origin Response" event. In the function, check if the response is a 404 and the request URI starts with /app; if so, modify the response to return your SPA's index.html with a 200 status code. Deploy this Lambda function to your CloudFront distribution, associating it with the cache behavior for your SPA.

As example of your Lambda@Edge:

exports.handler = (event, context, callback) => {
    const request = event.Records[0].cf.request;
    const response = event.Records[0].cf.response;

    if (response.status === '404' && request.uri.startsWith('/app')) {
        response.status = '200';
        response.statusDescription = 'OK';
        response.headers['content-type'] = [{ key: 'Content-Type', value: 'text/html' }];
        response.body = 'Your SPA index.html content goes here';
    }

    callback(null, response);
};

Key Sources:

profile picture
EXPERT
answered a month ago
AWS
EXPERT
reviewed a month ago
  • Thank you Osvaldo, this almost gets me there. The only part of this solution that I am stuck on is this line: response.body = 'Your SPA index.html content goes here';

    Does this imply I have to hardcode the content of index.html from the SPA bucket in the lambda function? I am wondering if there is any way to avoid this.

  • Sure, please update the response.body by fetching the object from your S3 bucket. Here's an example to guide you:

    const s3 = new AWS.S3();
    
    if (response.status === '404' && request.uri.startsWith('/app')) {
        const bucketName = 'Your bucket name here';
        const key = 'index.html';
    
        try {
            const object = await s3.getObject({ Bucket: bucketName, Key: key }).promise();
            response.body = object.Body.toString('utf-8'); // Set the response body to the content of index.html
        } catch (error) {
            console.error(`Error fetching index.html from S3: ${error}`);
        }
    }
    
    callback(null, response);
  • Ended up solving it using a different approach, see below.

0

For anyone stumbling upon this, here is how I ended up solving it:

Since my SPA origin should always serve index.html regardless of the path, instead of intercepting the response at error-time, I simply change the URI for all requests to the path to /app/index.html. That way, when CloudFront receives the path /app/page1 or /app/page2, etc., they are all converted to /app/index.html under the hood. I implemented this using a simple Lambda@Edge function, like this:

export const handler = (event, context, callback) => {
    const request = event.Records[0].cf.request;
    request.uri = "/app/index.html"
    return callback(null, request);
};

I then associated this function to both the /app and /app/* behavior in my CloudFront distribution.

My only lingering question is why a vanilla CloudFront function didn't work. I tried this:

function handler(event) {
    var request = event.request;
    var uri = request.uri;

    if (uri.startsWith("/app")) {
        request.uri = "/app/index.html"
    }
    return request
}

But it would give me 404 errors when I tried to use it...

Cole
answered a month ago

You are not logged in. Log in to post an answer.

A good answer clearly answers the question and provides constructive feedback and encourages professional growth in the question asker.

Guidelines for Answering Questions