301 redirects for files hosted in Amazon S3 using CloudFront Functions

The pur­pose of this guide is to walk you through the step-by-step process of cre­at­ing 301 redi­rects for files host­ed in Ama­zon S3 using Cloud­Front functions.

For more infor­ma­tion on the dif­fer­ences between Cloud­Front func­tions and Lambda@Edge Func­tions, please vis­it this link.

Cre­ate the func­tion #

  1. Go to your AWS dash­board and search for CloudFront. 
  2. Once there, on the left side­bar click on Functions -> Create Function. After giv­ing a name and descrip­tion to your func­tion, you will be redi­rect­ed to the edi­tor code with a basic code sample.

CloudFront Functions Base Code This base code will stop serv­ing the final asset and will instead return an OK screen.

Han­dling Requests #

The func­tion receives an event, and with­in this event lies the request data. The pro­vid­ed code offers 3 sam­ples on how to han­dle the requests:

  1. If the request URI con­tains /assets/old-slug/ redi­rect to /assets/new-slug/
  2. If the request URI con­tains one of the URLs pro­vid­ed in a vari­able, redi­rect to anoth­er asset.
  3. If the request URI ends with a known string, redi­rect by replac­ing the end­ing of the asset’s name.

Redi­rect­ing an asset is not lim­it­ed to these 3 sam­ples, you have the flex­i­bil­i­ty to redi­rect to alter­na­tive buck­ets or domains, mod­i­fy asset head­er respons­es, imple­ment cache key nor­mal­iza­tion, han­dle HTTP to HTTPS requests, and explore var­i­ous oth­er cus­tomiza­tion options. Giv­en that the code is writ­ten in pure JavaScript, you have the free­dom to use inno­v­a­tive approach­es to achieve your desired solutions.

function handler(event) {
    // NOTE: This example function is for a viewer request event trigger. 
    // Choose viewer request for event trigger when you associate this function with a distribution. 
    var domain = 'https://myS3bucketDomain.com';
    var currentFolderUri = '/assets/old-slug/';
    var newFolderUri = '/assets/new-slug/';
    var request = event.request;
    var newUri = request.uri;
    
    var brokenUrls = [
        {
            "/assets/folder1/yourFilename1.pdf":"/assets/new-slug/yourFilename1.pdf",
            "/assets/folder1/yourFilename2.pdf":"/assets/new-slug/yourFilename2.pdf",
            "/assets/folder1/yourFilename3.pdf":"/assets/new-slug/yourFilename3.pdf",
            "/assets/folder1/yourFilename4.pdf":"/assets/new-slug/yourFilename4.pdf",
            "/assets/temp-folder/yourFilenameA.pdf":"/assets/new-slug/yourFilenameA.pdf",
            "/assets/temp-folder/yourFilenameB.pdf":"/assets/new-slug/yourFilenameB.pdf",
            "/assets/temp-folder/yourFilenameC.pdf":"/assets/new-slug/yourFilenameC.pdf",
            "/assets/temp-folder/yourFilenameD.pdf":"/assets/new-slug/yourFilenameD.pdf"
        }
    ];
    
    if(request.uri.includes(currentFolderUri)){
        newUri = request.uri.replace(currentFolderUri, newFolderUri)
        return {
            statusCode: 301,
            statusDescription: 'Moved Permanently',
            headers: {
                location: {
                    value: domain + newUri,
                },
            },
        };
    }
    
    //fully change a filename
    if (brokenUrls[0][request.uri] !== undefined) {
        return {
            statusCode: 301,
            statusDescription: 'Moved Permanently',
            headers: {
                location: {
                    value: domain + brokenUrls[0][request.uri],
                },
            },
        };
    }
    else if (request.uri.endsWith('-categoryA.pdf')) {
        newUri = request.uri.replace('-categoryA.pdf', '-categoryB.pdf')
        return {
            statusCode: 301,
            statusDescription: 'Moved Permanently',
            headers: {
                location: {
                    value: domain + newUri,
                },
            },
        };
    }
  
    return request;
}

From lines 23 to 34, it redi­rects any request that con­tains a URI with /assets/old-slug/ to /assets/new-slug/.

From lines 37 to 47, it redi­rects any request that match­es any URI in the vari­able brokenUrls to its new URL.

From lines 48 to 59, it redi­rects any request that ends with -categoryA.pdf to a new URL for it, end­ing with -categoryB.pdf.

Last­ly, line 61 return request; will allow all the oth­er requests to keep going with their nor­mal route.

How to test your code. #

Since the lan­guage is Javascript, you can use console.log() to print and debug any variable.

For exam­ple: consoel.log(request);

Once you have your code in place, click Save Changes and go to the tab Test. (If you don’t hit Save Changes, you won’t see the changes on the Test tab). Write one of your pos­si­ble cas­es on the field URL Path, for exam­ple /assets/old-slug/testing.pdf, and click Test Function.

CloudFront Test Function

Beneath the form, you’ll find a sec­tion dis­play­ing the Execution result. If your code includes a console.log() state­ment, the cor­re­spond­ing out­put will be vis­i­ble in the Execution logs (I includ­ed the request).

And under Output you will see the response of your func­tion. For this case, redi­rect from myS3bucketDomain.com/assets/old-slug/testing.pdf to myS3bucketDomain.com/assets/new-slug/testing.pdf

Pub­lish­ing the Func­tion #

Once you are con­fi­dent with your code and after test­ing dif­fer­ent pos­si­ble requests, it’s time to pub­lish your func­tion. Go to the tab Publish and click Publish function.

Although, the func­tion is now avail­able to be used, it has not been assigned to a dis­tri­b­u­tion. To asso­ciate it with a dis­tri­b­u­tion, nav­i­gate to the Publish tab, click on Add Association and choose your Cloud­Front dis­tri­b­u­tion (you may need to open a new tab to copy the dis­tri­b­u­tion ID). Select the appro­pri­ate Event type (in this case, View­er Request”) and con­fig­ure the Cache behavior.

CloudFront Function Distribution

After you click on Add Association, head to your dis­tri­b­u­tion and click on the tab Invalidations. Cre­ate a new inval­i­da­tion con­tain­ing the URI of your old files, this will ensure the cache for those files is reset.

Why did we end up using Lambda@Edge? #

This client had about 2000 unique redi­rects to be val­i­dat­ed. These URLs would have to be defined on the vari­able brokenUrls of our code, due to Cloud­Front Func­tions size lim­i­ta­tions, this vari­able made the file too big for the avail­able scope.

If you have any ques­tions, feel free to con­tact me.


Claudia Aguilar

Partner, Software Engineer