Skip to content

Commit

Permalink
feat: add cache config
Browse files Browse the repository at this point in the history
  • Loading branch information
lewislbr committed Oct 25, 2021
1 parent 714cb4d commit 4b4ec65
Show file tree
Hide file tree
Showing 17 changed files with 120 additions and 55 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ GSS (Go serve SPA) is a containerized web server for single-page applications wr

- Optimized for single-page apps.
- Automatically serves pre-compressed brotli and gzip files if available.
- Sensible default cache configuration.
- Docker-based.
- Configurable via YAML.
- Lightweight.
Expand Down
92 changes: 51 additions & 41 deletions gss.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,68 +129,78 @@ func (a *app) serveSPA() http.HandlerFunc {
reqFile = filepath.Join(a.Config.Dir, "index.html")
}

serveCompressedFile := func(encoding, extension string) {
serve := func(mimeType string) {
serveFile := func(mimeType string) {
files, err := filepath.Glob(a.Config.Dir + "/*")
if err != nil {
log.Error().Msgf("Error getting files to serve: %v", err)
}

encodings := r.Header.Get("Accept-Encoding")
brotli := "br"
brotliExt := ".br"
gzip := "gzip"
gzipExt := ".gz"
serveCompressed := func(encoding, extension string) {
w.Header().Set("Content-Encoding", encoding)
w.Header().Set("Content-Type", mimeType)

http.ServeFile(w, r, reqFile+extension)
}

switch filepath.Ext(reqFile) {
case ".html":
serve("text/html")
case ".css":
serve("text/css")
case ".js":
serve("application/javascript")
case ".svg":
serve("image/svg+xml")
default:
http.ServeFile(w, r, reqFile)
}
}

encodings := r.Header.Get("Accept-Encoding")
files, err := filepath.Glob(a.Config.Dir + "/*")
if err != nil {
log.Error().Msgf("Error getting files to serve: %v", err)
}
if strings.Contains(encodings, brotli) {
for _, f := range files {
if f == reqFile+brotliExt {
serveCompressed(brotli, brotliExt)

brotli := "br"
brotliExt := ".br"
return
}
}
}

if strings.Contains(encodings, brotli) {
for _, f := range files {
if f == reqFile+brotliExt {
serveCompressedFile(brotli, brotliExt)
if strings.Contains(encodings, gzip) {
for _, f := range files {
if f == reqFile+gzipExt {
serveCompressed(gzip, gzipExt)

return
return
}
}
}

// If the request does not accept compressed files, or the directory does not contain compressed files,
// serve the file as is.
http.ServeFile(w, r, reqFile)
}

gzip := "gzip"
gzipExt := ".gz"
switch filepath.Ext(reqFile) {
case ".html":
w.Header().Set("Cache-Control", "no-cache")

if strings.Contains(encodings, gzip) {
for _, f := range files {
if f == reqFile+gzipExt {
serveCompressedFile(gzip, gzipExt)
serveFile("text/html")
case ".css":
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")

return
}
}
}
serveFile("text/css")
case ".js":
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")

// If the request does not accept compressed files, or the directory does not contain compressed files,
// serve the file as is.
http.ServeFile(w, r, reqFile)
serveFile("application/javascript")
case ".svg":
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")

serveFile("image/svg+xml")
default:
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")

http.ServeFile(w, r, reqFile)
}
}
}

func (a *app) setHeaders(h http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Vary", "Accept-Encoding")

for k, v := range a.Config.Headers {
w.Header().Set(k, v)
}
Expand Down
73 changes: 62 additions & 11 deletions gss_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,12 @@ func TestGSS(t *testing.T) {

app := newApp(cfg).init()
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/main.css", nil)
r := httptest.NewRequest(http.MethodGet, "/main.68aa49f7.css", nil)

app.Server.Handler.ServeHTTP(w, r)

assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "/main.css", r.RequestURI)
assert.Equal(t, http.StatusOK, w.Result().StatusCode)
assert.Equal(t, "/main.68aa49f7.css", r.RequestURI)
assert.Contains(t, w.Header().Get("Content-Type"), "css")
})

Expand All @@ -135,12 +135,12 @@ func TestGSS(t *testing.T) {

app := newApp(cfg).init()
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/main.js", nil)
r := httptest.NewRequest(http.MethodGet, "/main.8d3db4ef.js", nil)

app.Server.Handler.ServeHTTP(w, r)

assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "/main.js", r.RequestURI)
assert.Equal(t, http.StatusOK, w.Result().StatusCode)
assert.Equal(t, "/main.8d3db4ef.js", r.RequestURI)
assert.Contains(t, w.Header().Get("Content-Type"), "javascript")
})

Expand All @@ -156,12 +156,12 @@ func TestGSS(t *testing.T) {

app := newApp(cfg).init()
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/main.js.LICENSE.txt", nil)
r := httptest.NewRequest(http.MethodGet, "/main.8d3db4ef.js.LICENSE.txt", nil)

app.Server.Handler.ServeHTTP(w, r)

assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "/main.js.LICENSE.txt", r.RequestURI)
assert.Equal(t, http.StatusOK, w.Result().StatusCode)
assert.Equal(t, "/main.8d3db4ef.js.LICENSE.txt", r.RequestURI)
})

t.Run("serves brotli files succesfully", func(t *testing.T) {
Expand All @@ -176,7 +176,7 @@ func TestGSS(t *testing.T) {

app := newApp(cfg).init()
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/main.js", nil)
r := httptest.NewRequest(http.MethodGet, "/main.8d3db4ef.js", nil)

r.Header.Add("Accept-Encoding", "br")

Expand All @@ -198,7 +198,7 @@ func TestGSS(t *testing.T) {

app := newApp(cfg).init()
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/main.js", nil)
r := httptest.NewRequest(http.MethodGet, "/main.8d3db4ef.js", nil)

r.Header.Add("Accept-Encoding", "gzip")

Expand Down Expand Up @@ -247,4 +247,55 @@ func TestGSS(t *testing.T) {

assert.Equal(t, http.StatusNotFound, w.Code)
})

t.Run("serves a cached response for a fresh resource", func(t *testing.T) {
t.Parallel()

cfg := &config{
Dir: "test/web/dist",
}
cfg, err := cfg.validate()

assert.NoError(t, err)

app := newApp(cfg).init()

t.Run("HTML files should have Cache-Control: no-cache", func(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/", nil)

app.Server.Handler.ServeHTTP(w, r)

last := w.Header().Get("Last-Modified")

w = httptest.NewRecorder()
r = httptest.NewRequest(http.MethodGet, "/", nil)

r.Header.Set("If-Modified-Since", last)

app.Server.Handler.ServeHTTP(w, r)

assert.Equal(t, http.StatusNotModified, w.Code)
assert.Equal(t, w.Header().Get("Cache-Control"), "no-cache")
})

t.Run("other files should have Cache-Control: public, max-age=31536000, immutable", func(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/main.8d3db4ef.js", nil)

app.Server.Handler.ServeHTTP(w, r)

last := w.Header().Get("Last-Modified")

w = httptest.NewRecorder()
r = httptest.NewRequest(http.MethodGet, "/main.8d3db4ef.js", nil)

r.Header.Set("If-Modified-Since", last)

app.Server.Handler.ServeHTTP(w, r)

assert.Equal(t, http.StatusNotModified, w.Code)
assert.Equal(t, w.Header().Get("Cache-Control"), "public, max-age=31536000, immutable")
})
})
}
2 changes: 1 addition & 1 deletion test/web/dist/index.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="UTF-8"/><title>Project Title</title><meta name="viewport" content="width=device-width,initial-scale=1"/><script defer="defer" src="main.js"></script><link href="main.css" rel="stylesheet"></head><body><noscript>JavaScript is needed to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="UTF-8"/><title>Project Title</title><meta name="viewport" content="width=device-width,initial-scale=1"/><script defer="defer" src="main.8d3db4ef.js"></script><link href="main.68aa49f7.css" rel="stylesheet"></head><body><noscript>JavaScript is needed to run this app.</noscript><div id="root"></div></body></html>
Binary file modified test/web/dist/index.html.br
Binary file not shown.
Binary file modified test/web/dist/index.html.gz
Binary file not shown.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion test/web/dist/main.js → test/web/dist/main.8d3db4ef.js

Large diffs are not rendered by default.

File renamed without changes.
File renamed without changes.
Binary file added test/web/dist/main.8d3db4ef.js.br
Binary file not shown.
Binary file added test/web/dist/main.8d3db4ef.js.gz
Binary file not shown.
Binary file removed test/web/dist/main.js.br
Binary file not shown.
Binary file removed test/web/dist/main.js.gz
Binary file not shown.
5 changes: 4 additions & 1 deletion test/web/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module.exports = {
mode: "production",
entry: "./src/index.jsx",
output: {
filename: "[name].[contenthash:8].js",
path: path.join(__dirname, "/dist"),
},
module: {
Expand Down Expand Up @@ -43,7 +44,9 @@ module.exports = {
},
template: "./src/index.html",
}),
new MiniCssExtractPlugin(),
new MiniCssExtractPlugin({
filename: "[name].[contenthash:8].css",
}),
new CompressionPlugin(),
new CompressionPlugin({
algorithm: "brotliCompress",
Expand Down

0 comments on commit 4b4ec65

Please sign in to comment.