Alejandro Ciniglio

Fall back to default page when nginx proxy fails

I started using react router for the first time on a new web app recently. React router loads when your base page loads and then handles loading subviews as the URL changes. If using a singe JS bundle, this means that your server only serves the initial index page to the client.

This is easy to handle if the user will always start the app at the root page (e.g. http://example.com/), however, if they’ve navigated and want to refresh (or if they’re navigating back to the app via a bookmark), we need to handle that correctly on the server side.

For example, if a user navigates to http://example.com/subscribe, the server needs to return the index page that contains react router, then react router will load the subscribe view.

This is straightforward with nginx1:

location / {
    try_files index.html =404;
}

Things get a bit more hairy when we throw a backend API server into the mix.

In my case, I have an API backend that I want to be making calls to from the client on the same url. E.g. POST http://example.com/user/1/subscriptions.

This backend is a separate server listening on the machine, so we’ll use nginx’s proxy_pass to send requests and return them to the frontend.

The easiest way I found to do this was to make a separate named location block and refer to it from the main location block.

Failing attempts

This inital attempt fails to render anything other than API results.

location / {
    try_files @api index.html =404;
}

location @api {
    proxy_pass http://127.0.0.1:3000;
}

To fix this, we need to try to render static files if they exist, and only proxy to the API if there is no file with that name2.

location / {
    try_files $uri $uri/ @api index.html =404;
}

location @api {
    proxy_pass http://127.0.0.1:3000;
}

This works for the initial load, but now our refresh case doesn’t work because e.g. there’s no file named subscribe, so subscribe gets called on the api, but that’s not a valid API route either.

Both API calls and navigation work correctly

To get the navigation to work again, we’ll have our API return 404 for routes it can’t handle, then we’ll have nginx render index.html if the API page has a 404 error.

The working block is below, note that we also set some headers as good practice (forwarded-for can be used by the API server to determine the original request).

proxy_intercept_errors_on tells nginx that it should be responsible for handling proxy error codes, instead of letting the API server handle them directly.

location / {
    try_files $uri $uri/ @api index.html;
}

location @api {
    proxy_intercept_errors on;
    proxy_set_header X-Real-IP  $remote_addr;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://127.0.0.1:3000;
    error_page 404 /index.html;
}

  1. The =404 at the end tells nginx to return HTTP 404 if index.html isn’t found on disk (which should never happen). ↩︎

  2. This means our api endpoints can’t have the same path as a real file, for better or worse. ↩︎