Skip to content

Instantly share code, notes, and snippets.

@philfreo
Last active February 3, 2020 23:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save philfreo/2b168598a42364870632d10534299d16 to your computer and use it in GitHub Desktop.
Save philfreo/2b168598a42364870632d10534299d16 to your computer and use it in GitHub Desktop.
Lambda@Edge Dynamic Redirect Example

Server-side Redirects

See redirects.json to define or update redirects.

Redirects format

Example for redirects.json

{
  "/jobs": { "to": "https://jobs.example.com/", "statusCode": 301 },
  "/resources/the-follow-up-formula": { "to": "/resources/followup", "statusCode": 301 }
}

Each line is in the format of:

"REDIRECT_FROM_PATH": { "to": "REDIRECT_TO", "statusCode": HTTP_STATUS_CODE },

Rules:

  • redirects.json must be valid JSON. No comments in file.
    • The last entry may not have a trailing comma!
  • The REDIRECT_FROM_PATH key:
    • SHOULD be a URL Path, starting with a leading slash (good: /jobs; bad: jobs; bad: https://close.com/jobs)
    • SHOULD NOT include trailing slash (good: /jobs; bad: /jobs/)
    • SHOULD NOT include any querystring (good: /jobs; bad: /jobs/?utm_content)
  • The value should be an object with:
    • A to field, which:
      • Can be either a path (/pricing/ or /resources/sales/) or a full URL (https://close.com/jobs)
      • Can optionally include a querystring (/pricing/?source=old-pricing)
    • A statusCode field which supports values of 301 (permanent redirect) or 302 or 307 (temporary redirects)

Limitations / Future Enhancements

  • If we ever need to pass along, or merge, the request querystring to the redirect URL, that will require some changes to how our redirect system works.
  • Currently the redirects.json file is publicly accessible, so don't put anything sensitive/secret there.

Deployment

Changes to redirects.json get deployed automatically (by CircleCI) along with the rest of the site. This means you can simply add a new line to redirects.json, push to master, and within a few minutes (after Cloudfront cache invalidation is complete) the redirect should be live.

Under the hood

This works because of a Lambda@Edge function attached to our Cloudfront distribution.

Currently changes to the redirects function logic are deployed manually.

'use strict';
const REDIRECTS_DATA_REGION = 'us-east-1';
const REDIRECTS_DATA_BUCKET = 'example.com';
const REDIRECTS_DATA_OBJECT = 'config/redirects.json';
const AWS = require('aws-sdk');
const s3 = new AWS.S3({ region: REDIRECTS_DATA_REGION });
async function readFile(bucketName, filename) {
const params = { Bucket: bucketName, Key: filename };
const response = await s3.getObject(params, function (err, data) {
if (err) {
console.error('s3.getObject error: ' + err);
}
}).promise();
return response.Body.toString();
}
async function getRedirectsData() {
const txt = await readFile(REDIRECTS_DATA_BUCKET, REDIRECTS_DATA_OBJECT);
let data;
try {
data = JSON.parse(txt);
} catch (e) {
console.error('getRedirectsData parse failed', txt, e);
data = {};
}
return data;
}
const supportedStatusCodes = {
301: 'Moved Permanently',
302: 'Found',
307: 'Temporary Redirect'
}
exports.handler = async(event, context) => {
//console.log('event: ' + JSON.stringify(event));
//console.log('context: ' + JSON.stringify(context));
const redirectsData = await getRedirectsData();
// Get the incoming request and the initial response from S3
// https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html
const { request, response } = event.Records[0].cf;
// Enable HSTS
response.headers['strict-transport-security'] = [{ key: 'Strict-Transport-Security', value: 'max-age=63072000' }];
// request.uri is just the URL path without hostname or querystring
let path = request.uri
// Cut off trailing slash to normalize it
if (path.slice(-1) === '/') {
path = path.slice(0, -1);
}
// We might have wanted the lookup / key in redirectsData to be a full URL instead
// of a path, however with Origin Response as the trigger not sure how to get the
// incoming Host (versus the S3 bucket host). And probably it's fine as is.
const redirectData = redirectsData[path];
// See if a redirect exists & sanity check that the redirect object is what we expect
const shouldRedirect = !!(redirectData && redirectData.to && supportedStatusCodes[redirectData.statusCode]);
if (shouldRedirect) {
// Currently we assume that we don't want to pass along the request querystring, and instead
// we are choosing to support the ability for our defined redirect to include its own querystring
// However we might need further consideration about whether we ever need to support keeping/merging with request querystring isntead.
// Note: We can't completely create a new set of headers even for a redirect
// because Cloudfront fails painfully if you set (or clear) a "read-only" header.
// For Origin Response triggers, that includes leaving "Transfer-Encoding" alone.
// https://github.com/awsdocs/amazon-cloudfront-developer-guide/blob/master/doc_source/lambda-requirements-limits.md#read-only-headers
response.headers['location'] = [{
key: 'Location',
value: redirectData.to,
}];
return {
status: redirectData.statusCode,
statusDescription: supportedStatusCodes[redirectData.statusCode],
headers: response.headers,
};
}
return response;
};
{
"/jobs": { "to": "https://jobs.example.com/", "statusCode": 301 },
"/resources/the-follow-up-formula": { "to": "/resources/followup", "statusCode": 301 },
"/KEEP_AS_THE_LAST_ENTRY": { "to": "/IT_HELPS_US_AVOID_TRAILING_COMMA_ISSUES", "statusCode": 301 }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment