Files
mangaBookmark/backend/middleware.go
T
sulthan 9158709dc9 Gzip the text responses
Nothing was compressed: the list page shipped ~123 KB of HTML, CSS and JS in
the clear on every cold load, which is the wrong bill for a phone on mobile
data. A stdlib middleware compresses the four content types this server
actually serves in bulk and leaves woff2 alone, since it is already compressed.

Measured on the live server: style.css 21.8 KB -> 6.5, htmx 50.9 -> 16.4,
filter.js 7.6 -> 2.9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 22:23:14 +07:00

120 lines
3.5 KiB
Go

package main
import (
"compress/gzip"
"crypto/subtle"
"net/http"
"strings"
)
const bearerPrefix = "Bearer "
// withAuth guards a handler with a constant-time bearer-token check.
func withAuth(token string, next http.Handler) http.Handler {
want := []byte(token)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := r.Header.Get("Authorization")
if !strings.HasPrefix(h, bearerPrefix) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
got := []byte(strings.TrimPrefix(h, bearerPrefix))
if subtle.ConstantTimeCompare(got, want) != 1 {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// gzipWriter compresses the body and drops Content-Length, which no longer
// describes what goes on the wire. WriteHeader is where the decision is made:
// only then is Content-Type known.
type gzipWriter struct {
http.ResponseWriter
gz *gzip.Writer
decided bool
}
// compressible covers what this server actually serves in bulk: HTML, CSS, JS
// and JSON. Fonts are woff2, which is already compressed — gzipping them costs
// CPU to add bytes.
func compressible(contentType string) bool {
ct, _, _ := strings.Cut(contentType, ";")
switch strings.TrimSpace(ct) {
case "text/html", "text/css", "text/javascript", "application/javascript",
"application/json", "text/plain":
return true
}
return false
}
func (w *gzipWriter) WriteHeader(status int) {
if !w.decided {
w.decided = true
if compressible(w.Header().Get("Content-Type")) {
w.Header().Set("Content-Encoding", "gzip")
w.Header().Del("Content-Length")
w.gz = gzip.NewWriter(w.ResponseWriter)
}
}
w.ResponseWriter.WriteHeader(status)
}
func (w *gzipWriter) Write(b []byte) (int, error) {
if !w.decided {
w.WriteHeader(http.StatusOK)
}
if w.gz != nil {
return w.gz.Write(b)
}
return w.ResponseWriter.Write(b)
}
// withGzip compresses text responses for clients that ask. The templates,
// stylesheet and htmx together are ~120 KB uncompressed and roughly a quarter
// of that gzipped, which is the difference between a fast and a slow first load
// on mobile data.
func withGzip(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
next.ServeHTTP(w, r)
return
}
w.Header().Add("Vary", "Accept-Encoding")
gw := &gzipWriter{ResponseWriter: w}
defer func() {
if gw.gz != nil {
gw.gz.Close()
}
}()
next.ServeHTTP(gw, r)
})
}
// withCORS reflects the request Origin only when it is in allowed, answers
// preflight OPTIONS with 204, and passes everything else through. It wraps the
// auth middleware so preflight (which carries no Authorization header) is never
// rejected by auth.
func withCORS(allowed []string, next http.Handler) http.Handler {
set := make(map[string]struct{}, len(allowed))
for _, o := range allowed {
set[o] = struct{}{}
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if _, ok := set[origin]; ok && origin != "" {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Add("Vary", "Origin")
w.Header().Set("Access-Control-Allow-Methods", "GET,PUT,DELETE,OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Authorization,Content-Type")
w.Header().Set("Access-Control-Max-Age", "86400")
}
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}