Fixing the "cannot GET /URL" error on refresh with React Router (or how client side routers work)

6 min read.
This post is part of our React Router v4 course. We think it's pretty helpful. But this post is free, so carry on if you want.
Fixing the "cannot GET /URL" error on refresh with React Router (or how client side routers work) image

You’ve been working hard on a React app that uses React Router. Things are progressing nicely. You’re ahead of your deadline and you might just leave work early to enjoy those extra few hours of the weekend. You decide to do one last pass-through of your app before taking off. “That button should have a little more border radius to it.” you think. You change it, hit refresh, your app breaks. It’s not your typical error. If it were, you wouldn’t have sworn under your breath. Cannot read property 'state' of undefined, no worries. You’ve seen that one plenty of times. This one was different. It wasn’t even that the app crashed, it’s deeper than that. You stare at your monitor through your fingers. That’s it. That’s all you get. Your whole app breaks on refresh and all you get is three words.

Cannot GET /dashboard

“Probably a hot module replacement issue. Just an anomaly” - you optimistically convince yourself. To validate your assumption you restart the app. “Home page looks good. Navigating works fine. Let’s try refresh again.”

Cannot GET /settings

Defeat. No other words describe it so perfectly. There goes your long weekend. There might even go your whole weekend since you have literally no idea what’s going on. Luckily for you, you found this post. Meta, right?

First, let’s establish the problem you’re having. To do that, we’ll need to talk about how the browser and client side routers work.

In the old days, things were simple. If you wanted to get the contents of /dashboard, the browser would make a GET request to your server, by inspecting the path portion of the URL the server would figure out that the user was requesting the /dashboard page. It would then grab that page and send back to the browser as a response. Then these things called client side routers (CSR) came into the picture. With a CSR (like React Router), you’re no longer making requests to your server every time you change routes. Instead, your CSR is just handling that for you locally on the browser. So when you go to /dashboard, instead of making a GET request to your server, your CSR is using a browser API called history.pushState to manually change the URL and then it renders the View for that specific route - all without causing a page refresh.

Let’s look at that process a little more in depth.

The first time a user loads your app (i.e. visits your website), they don’t have any JavaScript loaded. That means no React and no React Router - so the first request will always be to your server. Then, assuming there was a successful GET request, all your JavaScript loads and React Router confidently hijacks your routing. From here on out, any other route changes in your app will be handled by React Router.

Notice the issue yet? React Router can only load after the first successful GET request to your server (or /). The reason for the dreaded Cannot GET /* error is because, if you’re at /dashboard and then hit refresh, the browser will make a GET request to /dashboard which will fail since you have no logic on your server for handling that request (since React Router is supposed to do it).

In case the issue is still fuzzy, here’s another example. Say you are really proud of the app you’ve been working on and you want to share it with your Mom. The app is Tic Tac Toe and has three routes, /, /play, and leaderboard. You send your Mom the link https://tictactyler.com/play since you want to play with her. When she enters that URL into her browser and hits enter, what happens? At this point she has no JavaScript, no React, and no React Router. The browser makes a GET request to /play and, since you’re relying on React Router to handle all the routing logic (but she has no React Router yet), the app crashes and she gets Cannot GET /play.

“Alright, alright, alright.” - Matthew Mcconaughey

Now the big question, how do we fix this?

The root of the issue is that you’re relying entirely on client side routing without setting up any logic to handle server side routing. There are two main ideas for solving this problem. The first, set up both client and server side routing. The second, redirect all server requests to /index.html which will download all the JS resources and allow React Router to take it from there. Most of the solutions we’ll look at involve the latter as it’s more simple.


Hash History

TBH, this one is kind of a hack. Have you ever seen those URLs with # in them? They’re using Hash History. The idea is by appending a # to the end of the root of your URL, anything after that # won’t be sent to the server. So if the URL was https://tm.io/#/courses, the browser would make a GET request to https://tm.io, get back all the JavaScript, React Router would then load, see /courses, and show the proper view for that route. React Router provides a HashRouter component you could use that will get you hash based routing, but honestly unless you REALLY need it, there are better options.


Catch-all

If you already have a server you’re using, this is probably your best bet. The main idea here is that you redirect all of your server requests to /index.html. The outcome is similar to Hash History. Any request that is made to your server will respond with the index page (and then fetch any JS resources you need), React Router will then take over and load the appropriate view. The actual code for this varies on which type of you have. Here are some examples

Express
app.get('/*', function(req, res) {
  res.sendFile(path.join(__dirname, 'path/to/your/index.html'), function(err) {
    if (err) {
      res.status(500).send(err)
    }
  })
})

Appache .htaccess

RewriteBase /
RewriteRule ^index\.html$ - [L]
 RewriteCond %{REQUEST_FILENAME} !-f
 RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]

Nginx .conf

location / {
  if (!-e $request_filename){
    rewrite ^(.*)$ /index.html break;
  }
}

No Server

For those blessed developers who don’t have to worry about managing a server, there are options for you as well and they (typically) come baked into the hosting service you’re using. There are obviously a lot of different variations here but you need to find a service that supports client side routers. For example, if you host with Firebase, one of the questions is asks you are

Configure as a single-page app (rewrite all urls to /index.html)?

Netlify also supports client-side routing, you just need to create a /_redirects file with the following rule

/*  /index.html  200

As you can probably guess, that tells Netlify to redirect all requests to .index.html.


Webpack / Development

This section is for everyone who ran into this problem in development using webpack-dev-server.. Just as above, what we need to do it tell Webpack Dev Sever to redirect all server requests to /index.html. There are just two properties in your webpack config you need to set to do this, publicPath and historyApiFallback.

publicPath: '/',
historyApiFallback: true,

publicPath allows you to specify the base path for all the assets within your application. historyAPIFallback will redirect 404s to /index.html.

Here’s an example of a basic webpack config file with both options in case you need it.

var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './app/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'index_bundle.js',
    publicPath: '/'
  },
  module: {
    rules: [
      { test: /\.(js)$/, use: 'babel-loader' },
      { test: /\.css$/, use: [ 'style-loader', 'css-loader' ]}
    ]
  },
  devServer: {
    historyApiFallback: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'app/index.html'
    })
  ]
};

That’s it. Go enjoy your weekend now 🍻.

Liked this post? Share it 🕺

Newsletter

Listen, I get it, newsletters are the worst.

This one is different though.

Stay up to date with the latest and greatest in the JavaScript ecosystem.

Already a member? Enter your email to check your score.

The Socials

The Newsletter