Why you should not use  FileServer in order to serve react application.
Tags: go, react, nginx
Published: 2022-01-23 17:48:20

While developing web server for reactjs I encoutered some unexpected issues and for a while I have been considering that I should not start using net/http at all.

There are tons of articles about “how to develop golang web application that will serve static files with net/http module“. Below I am going to explain why you should not do this.

### TL;DR

In order to serve static files you had better consider to use following:

- nginx
- Aws CloudFront / s3
- other server/cloud service

### Additional functionality.

It seems that net/http has all that you want. It has Fileserver, ... and so on. It provides additional features as like: content size, defining mime-types. But unfortunately you can’t disable it. E.g. if match can blow your mind. Browser expects content, but your server will respond 304  instead and you get blank page.

src/net/http/fs.go:470 

```go
func checkIfModifiedSince(r *Request, modtime time.Time) condResult {
	if r.Method != "GET" && r.Method != "HEAD" {
		return condNone
	}
	ims := r.Header.Get("If-Modified-Since")
	if ims == "" || isZeroTime(modtime) {
		return condNone
	}
	t, err := ParseTime(ims)
	if err != nil {
		return condNone
	}
	// The Last-Modified header truncates sub-second precision so
	// the modtime needs to be truncated too.
	modtime = modtime.Truncate(time.Second)
	if modtime.Before(t) || modtime.Equal(t) {
		return condFalse
	}
	return condTrue
}
```

above function checks "If-Modified-Since" header, then responds accordingly. However this code causes issue when your browser tries to load react application that was loaded earlier. You will see blank page, and you would have to reload page.

Primer grabbed from https://gist.github.com/paulmach/7271283:

```go
/*
Serve is a very simple static file server in go
Usage:
	-p="8100": port to serve on
	-d=".":    the directory of static files to host
Navigating to http://localhost:8100 will display the index.html or directory
listing file.
*/
package main

import (
	"flag"
	"log"
	"net/http"
)

func main() {
	port := flag.String("p", "8100", "port to serve on")
	directory := flag.String("d", ".", "the directory of static file to host")
	flag.Parse()

	http.Handle("/", http.FileServer(http.Dir(*directory)))

	log.Printf("Serving %s on HTTP port: %s\n", *directory, *port)
	log.Fatal(http.ListenAndServe(":"+*port, nil))
}
```

There is an issue in above code: If-Modified-Since issue.

How have i fixed this issue  in my project [https://github.com/Gasoid/scws/blob/main/handlers.go#L28](https://github.com/Gasoid/scws/blob/main/handlers.go#L28):

delete If-Modified-Since header:

```go
// ...

if r.Header.Get(ifModifiedSinceHeader) != "" && r.Method == http.MethodGet {
			r.Header.Del(ifModifiedSinceHeader)
		}

// ...
```

### ResponseWriter doesn’t cover all needs

Have you tried to catch status code with net/http package?

It is stupid but it is really complicated thing.

But why it can be needed?

- you are going to have logging (just simple access logs)
- you want to handle status code in middleware

Obviously responseWriter is intended only to write. Hence you need to use a proxy writer, e.g.:

```go
// original file is https://github.com/gin-gonic/gin/blob/master/response_writer.go

type ResponseWriter interface {
	http.ResponseWriter
	http.Hijacker
	http.Flusher
	http.CloseNotifier

	// Returns the HTTP response status code of the current request.
	Status() int

	// Returns the number of bytes already written into the response http body.
	// See Written()
	Size() int

	// Writes the string into the response body.
	WriteString(string) (int, error)

	// Returns true if the response body was already written.
	Written() bool

	// Forces to write the http header (status code + headers).
	WriteHeaderNow()

	// get the http.Pusher for server push
	Pusher() http.Pusher
}

type responseWriter struct {
	http.ResponseWriter
	size   int
	status int
}

//...

func (w *responseWriter) Status() int {
	return w.status
}

func (w *responseWriter) Size() int {
	return w.size
}
```

This code allows you to get status code and size when you need it.

However even though you can implement such a responseWriter it will return http response once your code writes either status or data. It means you are not able to substitute 404 or 403 errors.

### Slow HTTP request vulnerability

Let’s see Server struct:

```go
type Server struct {
	// ...

	ReadTimeout time.Duration
	WriteTimeout time.Duration

	//..
}
```

By default ReadTimeout and WriteTimeout have zero value. It means there will be no timeout.

So your application will have slow HTTP vulnerability.

> Slow HTTP attacks are **denial-of-service (DoS) attacks in which the attacker sends HTTP requests in pieces slowly**, one at a time to a Web server. If an HTTP request is not complete, or
if the transfer rate is very low, the server keeps its resources busy
waiting for the rest of the data.
> 

What i’ve done:
[https://github.com/Gasoid/scws/blob/main/scws.go#L51](https://github.com/Gasoid/scws/blob/main/scws.go#L51)

```go
func newServer(addr string, handler http.Handler) *http.Server {
	srv := &http.Server{
		ReadTimeout:  120 * time.Second,
		WriteTimeout: 120 * time.Second,
		IdleTimeout:  120 * time.Second,
		Handler:      handler,
		Addr:         addr,
	}
	return srv
}
```

### Mime types

Another small issue is lack of mime types. By default FileServer doesn’t give a proper mime type for files. It returns always a text type.

While building of docker image i add mime.types file [https://github.com/Gasoid/scws/blob/main/Dockerfile#L13](https://github.com/Gasoid/scws/blob/main/Dockerfile#L13)

```docker
#...

COPY mime.types /etc/mime.types

# ..
```

Despite the above, i used standart library for my own project. 

---

### Why i started developing SCWS: static content web server

Have you ever tried to publish REACT application?

You might be familiar how to set up nginx in order to serve react app. Let’s see.

site.conf:

```
server {
  listen 8080;
# Always serve index.html for any request
  location / {
    # Set path
    root /var/www/;
    try_files $uri /index.html;
  }
}
```

Dockerfile:

```docker

FROM node:16-stretch AS demo
WORKDIR /code
RUN git clone https://github.com/Gasoid/test-client.git
RUN cd test-client && npm install && npm run build

FROM nginx:1.16.1
COPY --from=demo /code/test-client/build/ /var/www/
ADD site.conf /etc/nginx/conf.d/site.conf
```

Then you can run it within docker:

```bash
docker build -t react-local:test .
docker run -p 8080:8080 react-local:test
```

Also for my production needs i need to have some features:

- prometheus metrics
- jaeger tracing
- health check

Nginx doesn’t have these features out of the box. So i have to install:

- https://github.com/opentracing-contrib/nginx-opentracing
- https://github.com/nginxinc/nginx-prometheus-exporter

SCWS has such features and more:

- prometheus metrics
- jaeger tracing
- health check
- settings for react app

I only want to describe the last feature.

For example, there are 2 environments: production and testing.

On production I have to show title “Production”, on testing - “Testing”.

In order to achive this i can use env variables from `process.env`.

But i would have to build image for 2 envs. So I would not able to use 1 docker image for testing and production.

How i solved this issue with settings feature

SCWS has built-in url: /_/settings. The url responds json containing env variables, e.g.: 

example:test

```docker
FROM node:16-stretch AS demo
WORKDIR /code
RUN git clone https://github.com/Gasoid/test-client.git
ENV REACT_APP_SETTINGS_API_URL="/_/settings"
RUN cd test-client && npm install && npm run build

FROM ghcr.io/gasoid/scws:latest
COPY --from=demo /code/test-client/build/ /www/
```

Production:

```bash
docker build -t example:test .
docker run -e SCWS_SETTINGS_VAR_TITLE="Production" -p 8080:8080 example:test
# get json
curl 127.0.0.1:8080/_/settings
```

JSON:

```json
{"TITLE":"Production"}
```

This feature allows to expose env vars with prefix **SCWS_SETTINGS_VAR_ .**

Your react app has to send GET request to url: /_/settings and then it will get json data.

If you find it interesting and useful, please get familiar with SCWS github repo https://github.com/Gasoid/scws.

Thanks for reading it.

---
Links:
- https://github.com/Gasoid/scws
- https://pkg.go.dev/net/http
- https://github.com/gin-gonic/gin/blob/master/response_writer.go
- https://gist.github.com/paulmach/7271283