DEV Community

Rinat Almakhov
Rinat Almakhov

Posted on

Why you should not use FileServer in order to serve react application.

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

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
}
Enter fullscreen mode Exit fullscreen mode

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:

/*
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))
}
Enter fullscreen mode Exit fullscreen mode

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:

delete If-Modified-Since header:

// ...

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

// ...
Enter fullscreen mode Exit fullscreen mode

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.:

// 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
}
Enter fullscreen mode Exit fullscreen mode

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:

type Server struct {
    // ...

    ReadTimeout time.Duration
    WriteTimeout time.Duration

    //..
}
Enter fullscreen mode Exit fullscreen mode

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

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
}
Enter fullscreen mode Exit fullscreen mode

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

#...

COPY mime.types /etc/mime.types

# ..
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Dockerfile:


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
Enter fullscreen mode Exit fullscreen mode

Then you can run it within docker:

docker build -t react-local:test .
docker run -p 8080:8080 react-local:test
Enter fullscreen mode Exit fullscreen mode

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:

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

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/
Enter fullscreen mode Exit fullscreen mode

Production:

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
Enter fullscreen mode Exit fullscreen mode

JSON:

{"TITLE":"Production"}
Enter fullscreen mode Exit fullscreen mode

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:

Discussion (0)