Cloudfront and index.html files

Posted on Oct 20, 2022
tl;dr: Deploy a simple function using CloudFormation to fix erroneous 404/403 errors in CloudFront....

CloudFront is not a Web Server.

It’s easy to think of Cloudfront as a web server - indeed once it’s set up it pretty much works as one, but behind the scenes it’s really just another interface to an S3 bucket, which itself you think looks like a filesystem, because it kinda pretends it is…

The problem comes, after you’re used to hitting /some-location/ and the webserver automagically knows to server you index.html (even though you never see this in the location bar of your browswer). Then you’ll be a little surprised to find that you don’t get served an index.html file at all - nor do you get a 404 error. What you actually get is a cryptic cloudfront access denied error message.

I’ve recently moved to using hugo and specifically tried the fix on that page. I wasn’t overly happy wth it, as the underlying issue still exists.

Compounding the issue is that a client requesting one of the urls that ‘doesn’t exist’ actually encounters a 403 at some point on the journey, as the underlying issue is actually a permission denied error (i.e. you’re attempting to read an S3 file you don’t have permission to!). This results in clients seeing exceptions from CloudFront with a harsh machine message of ‘Access Denied’. We’re going to fix all of this anyway, but a minor fix for this kinda thing is to change your config ever so slightly, mapping 403->404 errors, e.g.

        - ErrorCode: 403
          ResponseCode: 404
          ResponsePagePath: /404.html

Fortunately, there’s a pretty simple fix for this - use the exposed ‘functions’ that you get with cloudfront.

I’ll write a longer post and link to it here in the future about how to deploy an entire stack using cloudformation, but for now I’m going to assume you’re dropping a stack using CloudFormation.

AWS have some samples (as always) for this kind of thing, the one we want and need is this JS function

Next, let’s add the function to our existing template:

    Type: AWS::CloudFront::Function
      AutoPublish: true
      FunctionCode: |
        function handler(event) {
          var request = event.request;
          var uri = request.uri;
          // Check whether the URI is missing a file name.
          if (uri.endsWith('/')) {
              request.uri += 'index.html';
          // Check whether the URI is missing a file extension.
          else if (!uri.includes('.')) {
              request.uri += '/index.html';
          return request;
        Comment: !Sub 'Redirect to ${DomainName}'
        Runtime: cloudfront-js-1.0
      Name: !Sub "${AWS::StackName}-redirectFunction"

Note, you’ll need to pass in the ‘DomainName’ parameter. This would just be as simple as adding it to the Parameters section at the top of your template, or just hardcode or even delete the comment line if you can’t be bothered.

    Type: String
    Description: Target hosted domain for S3 / Cloudfront hosting
    Default: ""

Next up, amend your ‘AWS::CloudFront::Distribution’ section to include a call to this function.

    Type: "AWS::CloudFront::Distribution"
            - EventType: viewer-request
              FunctionARN: !GetAtt RedirectFunction.FunctionMetadata.FunctionARN

Roll this out again, and you should be good to go. URL’s which end in a ‘/’ will have index.html appended to them.