Serving Single-Page Applications in Go

Someone may ask what is so special about serving single-page applications? Well, if you use hash (#) based URL's like https://yourdomain.com/#/yourroute, there is no problem because browser recognize that this is a default page with some parameters and doesn't make another request to the server. However, if you want to use non-hash based routes, there is a problem if you try to refresh a non-default route. In this case browser's default action is to load such URL from the server and there is no such document on the server. For example, if you load your SPA at URL https://yourcomain.com and then you click on another route, you will see in your browser's URL field something like https://yourdomain.com/yourroute. That works fine unless you try to reload the page. In that case browser doesn't know this is actually already loaded SPA, but it actually tries to fetch /yourroute URI from server.  As there is no such URI on the server, you will usually get the famous 404 error and your application breaks.

In order to overcome this, your server should always return index.html or whatever document is default for your single-page application. Maybe not literally in all cases, maybe you want to use some patterns, but in lot of cases you actually want to return index.html in all 404 cases so that your SPA can continue normal execution. JavaScript based Web server usually have such options because they are very often used to serve SPA's, for example sirv-cli has option --single which will solve the problem. But can we do this in Go based server?

Go's standard library includes http.FileServer handler which can serve static files. However, it doesn't have possibility to override 404's with some default document. You can make a handler which first checks if requested URI (file) exists on the server and if not, respond with index.html, but this involves additional file system check which you may want to avoid for performance reasons. The question is, how can we solve this without additional filesystem check?

We can write our own implementation of http.ResponseWriter interface which will intercept 404 errors and respond with index.html in such cases. Let's see such an implementation:

type intercept404 struct {
	http.ResponseWriter
	statusCode int
}

func (w *intercept404) Write(b []byte) (int, error) {
	if w.statusCode == http.StatusNotFound {
		return len(b), nil
	}
	if w.statusCode != 0 {
		w.WriteHeader(w.statusCode)
	}
	return w.ResponseWriter.Write(b)
}

func (w *intercept404) WriteHeader(statusCode int) {
	if statusCode >= 300 && statusCode < 400 {
		w.ResponseWriter.WriteHeader(statusCode)
		return
	}
	w.statusCode = statusCode
}

The idea is to replace original http.ResponseWriter when invoking http.FileServer with the one above and in case of 404, replace the requested URI with index.html (actually /) and invoke http.FileServer again, but now with original http.ResponseWriter. Here is the http.HandlerFunc which implements this idea:

func spaFileServeFunc(dir string) func(http.ResponseWriter, *http.Request) {
	fileServer := http.FileServer(http.Dir(dir))
	return func(w http.ResponseWriter, r *http.Request) {
		wt := &intercept404{ResponseWriter: w}
		fileServer.ServeHTTP(wt, r)
		if wt.statusCode == http.StatusNotFound {
			r.URL.Path = "/"
			w.Header().Set("Content-Type", "text/html")
			fileServer.ServeHTTP(w, r)
		}
	}
}

Finally, here is the main function which implements the server itself:

func main() {
	http.HandleFunc("/", spaFileServeFunc("public"))

	log.Fatal(http.ListenAndServe(":3006", nil))
}

The  full implementation using Svelte as JavaScript frontend can be found here.

Someone may argue that we still have performance loses in case that document is not found in initial request, but this is again more or less just file system check. This is still better than doing the double file system check in all requests.