CloudFront function to support HTML5 History API

How to redirect origin requests to index.html

Author's image
Tamás Sallai
3 mins
Photo by Pixabay: https://www.pexels.com/photo/grey-concrete-3-baluster-near-mountains-and-sea-during-daytime-164268/

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
	}
}
July 11, 2023
In this article