CloudFront function to support HTML5 History API
How to redirect origin requests to index.html
Webapp hosting in AWS
A common setup is to host static files for a web application in an S3 bucket and then add CloudFront for HTTPS and custom domain support. This means users connect to AWS's global CDN and the files themselves are stored as objects in a bucket. The end result is a highly optimized solution where both the storage and the delivery can seamlessly scale to any load.
In this setup, CloudFront does request routing. When a request from a client for the index.html
hits CloudFront, it forwards it to the S3 API that in turn
returns the contents of the HTML. Similarly, requests for the assets/main.js
and favicon.ico
are routed to S3 with the path of the file. This allows
the clients to fetch all the static files.
HTML5 History API
The picture gets more complicated when the client uses the HTML5 History API. When the client-side app calls history.pushState()
the URL the browser shows
changes but there is no call to the backend. For example, if an Ecommerce app shows the orders of the current user at /orders
then when the user navigates
to that page the URL changes to <domain>/orders
.
This works because it is only a client-side behavior, CloudFront has nothing to do with it.
Until the user reloads the page, in which case the browser sends a request to the /orders
path. CloudFront then forwards it to the origin (S3), which in
turn returns an error as there is no such file.
CloudFront Functions
The solution is to configure CloudFront to fetch the /index.html
in cases like this. Usually, client-side apps restore their state from the URL, so it
does not matter that the same code is returned for different URLs.
CloudFront supports running arbitrary code for viewer requests and that also supports changing the origin request path. This is perfect for this use-case: the
function can decide whether the request is for a static file stored in S3, in which case it forwards it to S3 as-is, or it's for a navigation path, when it can
redirect it to the main index.html
.
The code for this function:
function handler(event) {
var request = event.request;
if (request.uri.match(/\/[^./]+\.[^./]+$/) === null) {
request.uri = "/index.html";
}
return request;
}
It extracts the event.request.uri
which is the path and tries to match the last path part to a filename with an extension. This will match things like
assets/main.js
or favicon.ico
, so files stored in S3 are accessible as before.
But if the URI does not match the pattern, the function rewrites it to /index.html
.
Resources with Terraform
Finally, let's see how the different parts are configured!
The function itself is a resource:
resource "aws_cloudfront_function" "history_api" {
name = "history_api-${random_id.id.hex}"
runtime = "cloudfront-js-1.0"
code = <<EOF
function handler(event) {
var request = event.request;
if (request.uri.match(/\/[^./]+\.[^./]+$/) === null) {
request.uri = "/index.html";
}
return request;
}
EOF
}
Then a cache behavior is configured to use it:
default_cache_behavior {
# ...
function_association {
event_type = "viewer-request"
function_arn = aws_cloudfront_function.history_api.arn
}
}