Skip to main content
Solving SPA Routing Challenges on AWS with CloudFront Functions
Photo by Adrien Bruneau on Unsplash

Solving SPA Routing Challenges on AWS with CloudFront Functions

·1403 words·7 mins· loading · loading ·
AWS aws spa cloudfront
Sergio Cambelo
Author
Sergio Cambelo
Cloud Architect
Table of Contents
Combine a SPA and an API under the same CloudFront distribution and don’t die in the process

Using CloudFront with an S3 bucket as the origin for storing Single Page Application (SPA) files is a popular approach that offers several benefits: it’s entirely serverless, meaning there’s no infrastructure to maintain, patch, or upgrade; it’s cost-efficient, as costs are based on usage; and it’s both flexible and scalable, with CloudFront and S3 capable of handling millions of user requests.

This architecture can be enhanced by adding an API Gateway API as a custom origin. This allows you to have both your frontend and backend under the same domain and certificate. Additionally, it offers the advantage of not having to deal with CORS configurations.

But, this solution has a small issue. Let’s see what happens. When you visit your newly deployed SPA and request a route, this occurs.

How this happened?
#

To comprehend what is happening, we need to understand what a route is in a SPA and how a request is processed by CloudFront and S3.

SPA routes
#

SPA routes, also known as client-side routing, are a mechanism that allows loading resources processed on the client side, meaning the user’s browser, without needing to send any requests to the server. Whenever a new route is requested in the user’s browser, the SPA JavaScript interprets it and renders the appropriate component.

CloudFront and S3 requests
#

In a CloudFront distribution with S3 as the origin, when a request reaches CloudFront, it first checks if the object is already in the cache. If not, it forwards the request to S3. In an SPA, if the requested resource is an object, like /index.html or /img/picture.png, S3 will return the object to the distribution cache, and CloudFront will return it to the requester. On the other hand, if the requested resource is a route, such as /route-1, and there is no corresponding object in S3, it will return a 403 error code.

A traditional solution to the issue of 403 error codes in SPAs hosted on AWS CloudFront is to use CloudFront’s custom error pages feature. This involves configuring CloudFront to respond to HTTP 403 errors (which are returned when a requested route doesn’t correspond to an object in the S3 bucket) with a redirect to the index.html file. While this method can solve the issue, it is not ideal for our architecture because it involves additional redirection, which can potentially slow down the application’s response time, and it doesn’t provide a seamless integration between the frontend and backend under the same domain and certificate.

In simpler terms, CloudFront’s custom error pages affect the entire distribution, which could potentially lead to unexpected behavior. If API Gateway returns a 403 error code due to unauthorized access, we will get a 200 HTTP code instead and the content of /index.html, preventing the requester (i.e., the frontend) from handling the error properly.

CloudFront Functions to the rescue
#

CloudFront Functions are a feature of AWS CloudFront that allows you to run lightweight JavaScript functions at the edge locations of the CloudFront network, close to your users. This feature is designed for high-scale, latency-sensitive operations like manipulating HTTP headers, URL rewrites, redirects, and other tasks that can be executed quickly without needing to access other resources or services. They are primarily used for handling viewer request and response events, enabling you to customize the content that CloudFront delivers.

CloudFront Functions and Lambda@Edge are both part of AWS’s suite of serverless solutions, but they are used for different purposes and have different execution environments.

Lambda@Edge is designed for more compute-heavy operations and has the ability to execute complex logic, including interacting with other AWS services. It also allows you to write functions in multiple languages, not just JavaScript.

On the other hand, CloudFront Functions are designed for lighter, latency-sensitive operations. However, CloudFront Functions only support JavaScript.

In terms of cost, CloudFront Functions are generally less expensive than Lambda@Edge, making them a more cost-effective choice for simple operations.

That makes CloudFront Functions a better choice to address our problem with SPA routes.

CloudFront Functions can be associated with two different events.

  • Viewer request: The function executes when CloudFront receives a request from a viewer, before it checks to see whether the requested object is in the CloudFront cache.

  • Viewer response: The function executes before returning the requested file to the viewer. Note that the function executes regardless of whether the file is already in the CloudFront cache.

Here is a simple function that can be associated with the viewer request event of our CloudFront distribution’s default behavior, where the S3 Origin is located. This function rewrites request route paths to /index.html.

var level = 0; // subdirectory level where index.html is located.
var regexExpr = /^\/.+(\.\w+$)/; // Regex expression than matches paths requestiong an object. i.e: /route1/my-picture.png

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

    if (isRoute(olduri)) { // if is a route request. i.e: /route1
        var defaultPath = '';
        
        var parts = olduri
            .replace(/^\//,'') // remove leading '/'
            .replace(/\/$/,'') // remove triling '/' if any
            .split('/'); // split uri into array of parts. i.e: ['route1', 'my-picture.png']
        
        var nparts = parts.length;

        // determine the limit as either level or nparts, whichever is lower
        var limit = (level <= nparts) ? level : nparts; 

        // build the default path. i.e: /route1
        for (var i = 0; i < limit; i++) {
            defaultPath += '/' + parts[i];
        }
        
        var newuri = defaultPath + '/index.html';

        request.uri = newuri;
        console.log('Request for [' + olduri + '], rewritten to [' + newuri + ']');
    }   

    return request;
}

// Returns true if uri is a route. i.e: /route1
// Returns false if uri is a file. i.e: /route1/index.html
function isRoute(uri) {
    return !regexExpr.test(uri);
}

How to deploy it
#

  1. Sign in to the AWS Management Console and open the CloudFront console at https://console.aws.amazon.com/cloudfront.

  2. In the navigation pane, choose Functions. Then choose Create function. Enter a function name, and then choose Continue. In Comment, enter a description for the function such as SPA routing function.

  3. Copy the function code.

  4. Paste the code into the code editor in the console, so that it replaces the default code in the editor.

  5. Choose Save.

  6. Publish: You must publish the function before you can associate it with your CloudFront distribution.

    Choose the Publish tab, then choose the Publish button to publish the function.

  7. Associate with a distribution or cache behavior: Still on the Functions page, choose the Publish tab. Choose the Add association button. Select a distribution and/or a cache behavior. Don’t change the event type.

    The Associated distributions table shows the associated distribution.

  8. Wait for deployment: Wait a few minutes for the associated distribution to finish deploying. Then to check the distribution’s status, select the distribution in the Associated distributions table and choose View distribution.

    When the distribution’s status is Deployed, you’re ready to verify that the function works.

So now, our architecture looks like this.

Now that you know how to deploy a CloudFront function in the console, to make the testing process easier here’s a link to a GitLab repository featuring this same architecture, ready to be deployed with AWS CDK.

Sergio Cambelo / aws-cloudfront-spa-routing

0
0

The repo includes the function code, a small Angular app for testing routes, and even an API Gateway with its own route resource /api/test that returns a 200 HTTP code to demonstrate that the function only affects the S3 Origin.

To deploy this sample CDK App, assuming you have NPM and CDK installed just clone the repo on your computer and follow this steps.

  1. Install libraries and dependencies with npm i.

  2. The first time you run CDK you need to bootstrap your AWS account with cdk bootstrap.

  3. If you whant to see what resources will be deployed, you could run a cdk diff.

  4. Finally deploy the cdk app with cdk deploy.

Play with it and have fun!

Conclusion
#

In conclusion, the challenge of 403 error codes encountered with Single Page Applications (SPAs) routing on AWS can be effectively addressed by using CloudFront Functions. This solution enables the rewriting of request route paths to /index.html, thus eliminating the errors and ensuring smooth operation of the SPAs.


References
#