How to configure CloudFront for the HTML5 history API

(Read this article on the blog) Navigation state in webapps Modern web applications handle their navigation state internally and don’t require page reloads when the user is moving between the pages. This is a convenient feature as that also means ephemeral state, such as text entered into input fields and popups are preserved. But that […]

(Read this article on the blog)

Modern web applications handle their navigation state internally and don’t require page reloads when the user is moving between the pages. This is a convenient
feature as that also means ephemeral state, such as text entered into input fields and popups are preserved. But that also means, by default, that the URL
that the users see stays the same and if they choose to reload the page they are taken back to the home page.

One solution for both of these problems was the use of fragments. This is the part that comes after the # and the webapp is free to change it and that
does not cause a page reload.

// url is: example.com
window.location.hash = "articles";
// url is: example.com/#articles

And if the user reloads the page, the webapp can restore the navigation by reading the fragment:

// url is: example.com/#articles
window.location.hash; // #articles

This was the de-facto solution before HTML5 History API. The problem with fragments is that they are different. Instead of having a “normal-looking” URL,
such as example.com/articles, it is example.com/#articles. Other than that, the browser does not send the fragment, so there is no way to provide
useful data for clients that don’t execute Javascript, such as web crawlers.

HTML5 History API

The History API provides a solution for all the problems with fragments and it is widely supported in browsers.
It provides a pushState that changes the URL in a way that does not trigger a page reload.

// url is: example.com
history.pushState(undefined, undefined, "/articles")
// url is: example.com/articles

But it also introduced a new problem: what happens if the user reloads the page?

With fragments, the HTTP request goes to the same URL for all pages. But with pushState, it’s no longer the case. Worse still, if the backend can not
serve a HTML page for the changed URLs it’s not immediately apparent.

Here’s a video showing the problem:

This article focuses on how to configure CloudFront for this scenario but the solution described here works for other backends too.

CloudFront

Let’s consider a typical setup: the frontend is hosted behind a CloudFront distribution and that uses the HTML5 History API to provide friendly URLs. By
default, refreshing the page returns an error.

browserCloudFrontbackendindex.htmlapp.jsOpen URL example.comGET /GET /index.htmlindex.htmlNavigate to /articlespushStateReload pageGET /articlesGET /articles404404

The first request works as when CloudFront forwards it to the origin the URL points to a file that exists on the backend. Navigation works as that does not
trigger a new request. But refresh is broken as the new URL does not point to a file on the backend.

One solution is to configure the backend to return the index.html for URLs that has no matching file. If you use Apache, Nginx, or another custom
webserver then you can check the documentation how to do it. But there are cases, such as for S3 that does not support this behavior. In that case, you need to
configure CloudFront to handle it internally.

Error response

CloudFront supports the notion of “error response”. If the origin returns a specific type of error, CloudFront can automatically modify the request and return a
different response.

To support HTML5 History API, configure a custom response for the 404 errors:

How this works?

CloudFront still sends a request to the backend and that responds with a 404 code. This triggers the custom error response and CloudFront sends another request
to the / path. The backend responds with the index.html and CloudFront returns that with the 200 status code.

browserCloudFrontbackendindex.htmlapp.jsReload pageGET /articlesGET /articles404GET /index.htmlindex.html

Alternative: CloudFront Functions

An alternative solution is to use CloudFront Functions to dynamically rewrite the request path. This way you have more control over which paths are changed and
which should generate 404 errors instead.

browserCloudFrontbackendindex.htmlapp.jsReload pageGET /articlesRun functionrequest.uri=”/”GET /index.htmlindex.html

Conclusion

The history.pushState method allows a web application to change the page URL without triggering a HTTP request. This allows nicer-looking navigation and
permalinks but if the user reloads the page the request goes to the path that is shown in the URL bar. If the backend does not support it, this results a 404
error.

CloudFront offers a custom error response setting to handle these errors. Alternatively, you can use CloudFront Functions to rewrite the URL before the request
is sent to the origin.

Source: Advanced Web Machinery