9

So I'm using a go server to serve up a single page web application.

This works for serving all the assets on the root route. All the CSS and HTML are served up correctly.

fs := http.FileServer(http.Dir("build"))
http.Handle("/", fs)

So when the URL is http://myserverurl/index.html or http://myserverurl/styles.css, it serves the corresponding file.

But for a URL like http://myserverurl/myCustompage, it throws 404 if myCustompage is not a file in the build folder.

How do I make all routes for which a file does not exist serve index.html?

It is a single page web application and it will render the appropriate screen once the html and js are served. But it needs index.html to be served on routes for which there is no file.

How can this be done?

Jeff P Chacko
  • 4,908
  • 4
  • 24
  • 32

1 Answers1

16

The handler returned by http.FileServer() does not support customization, it does not support providing a custom 404 page or action.

What we may do is wrap the handler returned by http.FileServer(), and in our handler we may do whatever we want of course. In our wrapper handler we will call the file server handler, and if that would send a 404 not found response, we won't send it to the client but replace it with a redirect response.

To achieve that, in our wrapper we create a wrapper http.ResponseWriter which we will pass to the handler returned by http.FileServer(), and in this wrapper response writer we may inspect the status code, and if it's 404, we may act to not send the response to the client, but instead send a redirect to /index.html.

This is an example how this wrapper http.ResponseWriter may look like:

type NotFoundRedirectRespWr struct {
    http.ResponseWriter // We embed http.ResponseWriter
    status              int
}

func (w *NotFoundRedirectRespWr) WriteHeader(status int) {
    w.status = status // Store the status for our own use
    if status != http.StatusNotFound {
        w.ResponseWriter.WriteHeader(status)
    }
}

func (w *NotFoundRedirectRespWr) Write(p []byte) (int, error) {
    if w.status != http.StatusNotFound {
        return w.ResponseWriter.Write(p)
    }
    return len(p), nil // Lie that we successfully written it
}

And wrapping the handler returned by http.FileServer() may look like this:

func wrapHandler(h http.Handler) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        nfrw := &NotFoundRedirectRespWr{ResponseWriter: w}
        h.ServeHTTP(nfrw, r)
        if nfrw.status == 404 {
            log.Printf("Redirecting %s to index.html.", r.RequestURI)
            http.Redirect(w, r, "/index.html", http.StatusFound)
        }
    }
}

Note that I used http.StatusFound redirect status code instead of http.StatusMovedPermanently as the latter may be cached by browsers, and so if a file with that name is created later, the browser would not request it but display index.html immediately.

And now put this in use, the main() function:

func main() {
    fs := wrapHandler(http.FileServer(http.Dir(".")))
    http.HandleFunc("/", fs)
    panic(http.ListenAndServe(":8080", nil))
}

Attempting to query a non-existing file, we'll see this in the log:

2017/11/14 14:10:21 Redirecting /a.txt3 to /index.html.
2017/11/14 14:10:21 Redirecting /favicon.ico to /index.html.

Note that our custom handler (being well-behaviour) also redirected the request to /favico.ico to index.html because I do not have a favico.ico file in my file system. You may want to add this as an exception if you don't have it either.

The full example is available on the Go Playground. You can't run it there, save it to your local Go workspace and run it locally.

Also check this related question: Log 404 on http.FileServer

icza
  • 389,944
  • 63
  • 907
  • 827
  • 1
    This is interesting. I'd figured out another approach where I use `http.ServeFile`. Where I first check if file exists using `os.Stat` and then serve the corresponding file or index.html. Could you also tell me which approach would be better and why? – Jeff P Chacko Nov 15 '17 at 06:19
  • @JeffPChacko The handler returned by `http.FileServer()` also uses `http.ServeFile()` (or parts of its implementation) under the hood, but also provides other goodies like directory listing and security to make sure no file is served outside of the given folder. If don't use `http.FileServer()`, you have to take care of these (e.g. you have to normalize / resolve tricky request paths containing `..`). – icza Nov 15 '17 at 07:21
  • I was looking for this, thanks, but I needed to make small modification to make it work with reactjs BrowserRouter so I changed those lines: ``` if nfrw.status == 404 { log.Printf("Redirecting %s to index.html.", r.RequestURI) http.Redirect(w, r, "/index.html", http.StatusFound) } ``` with this: ``` if nfrw.status == http.StatusNotFound { w.Header().Set("Content-Type", "text/html; charset=utf-8") http.ServeFile(w, r, "./web/static/index.html") } ``` so it serves index.html normally without redirection. – Phoebus Aug 28 '23 at 21:46