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
}
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))
}
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)
}
// ...
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
}
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
//..
}
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
}
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
# ..
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:
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:
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
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:
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:
{"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:
Top comments (0)