internal/httpd/compress.go

f327db6192d9a0877606a40385b24c0cda29fd2d
gitbay/internal/httpd/compress.go history · blame · raw

85 lines · 2221 bytes

 1package httpd
 2
 3import (
 4	"compress/gzip"
 5	"net/http"
 6	"strings"
 7)
 8
 9// compressed gzips text responses for clients that accept it. Every body
10// the forge writes itself is text — pages, the stylesheet, feeds, JSON —
11// and none is precompressed; the stylesheet alone went from 67 KB to 15 KB
12// (#232). The decision is made when the headers are final, on the content
13// type, so git transport, LFS and binary downloads pass through untouched.
14func compressed(next http.Handler) http.Handler {
15	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
16		if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
17			next.ServeHTTP(w, r)
18			return
19		}
20		cw := &gzipWriter{ResponseWriter: w}
21		defer cw.Close()
22		next.ServeHTTP(cw, r)
23	})
24}
25
26var compressibleTypes = []string{"text/", "application/json", "application/atom+xml", "image/svg+xml"}
27
28func compressible(contentType string) bool {
29	for _, p := range compressibleTypes {
30		if strings.HasPrefix(contentType, p) {
31			return true
32		}
33	}
34	return false
35}
36
37// gzipWriter decides on the first WriteHeader or Write whether the body
38// is compressed, then either wraps the underlying writer or steps aside.
39type gzipWriter struct {
40	http.ResponseWriter
41	gz      *gzip.Writer
42	decided bool
43}
44
45func (g *gzipWriter) decide(status int) {
46	if g.decided {
47		return
48	}
49	g.decided = true
50	h := g.Header()
51	if status == http.StatusNotModified || status == http.StatusNoContent ||
52		h.Get("Content-Encoding") != "" || !compressible(h.Get("Content-Type")) {
53		return
54	}
55	h.Del("Content-Length")
56	h.Set("Content-Encoding", "gzip")
57	h.Add("Vary", "Accept-Encoding")
58	g.gz = gzip.NewWriter(g.ResponseWriter)
59}
60
61func (g *gzipWriter) WriteHeader(status int) {
62	g.decide(status)
63	g.ResponseWriter.WriteHeader(status)
64}
65
66func (g *gzipWriter) Write(b []byte) (int, error) {
67	if !g.decided {
68		// net/http would sniff the type on this write; do it first so the
69		// decision sees it.
70		if g.Header().Get("Content-Type") == "" {
71			g.Header().Set("Content-Type", http.DetectContentType(b))
72		}
73		g.decide(http.StatusOK)
74	}
75	if g.gz == nil {
76		return g.ResponseWriter.Write(b)
77	}
78	return g.gz.Write(b)
79}
80
81func (g *gzipWriter) Close() {
82	if g.gz != nil {
83		g.gz.Close()
84	}
85}