internal/httpd/compress.go
109 lines · 2953 bytes
11 symbols in this file
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}
86
87// Flush sends what the gzip stream holds, then flushes the connection, so
88// a streamed page reaches the browser as it is written.
89func (g *gzipWriter) Flush() {
90 g.FlushError()
91}
92
93// FlushError is Flush with the error a failed flush produces.
94// http.ResponseController.Flush prefers this over Flush when both are
95// implemented, so a write that fails partway through a stream is reported
96// instead of silently dropped.
97func (g *gzipWriter) FlushError() error {
98 if !g.decided {
99 g.decide(http.StatusOK)
100 }
101 if g.gz != nil {
102 if err := g.gz.Flush(); err != nil {
103 return err
104 }
105 }
106 return http.NewResponseController(g.ResponseWriter).Flush()
107}
108
109func (g *gzipWriter) Unwrap() http.ResponseWriter { return g.ResponseWriter }