Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to custom handle a file not being found when using go static file server?

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?

like image 229
Jeff P Chacko Avatar asked Nov 14 '17 11:11

Jeff P Chacko


1 Answers

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

like image 99
icza Avatar answered Sep 22 '22 12:09

icza