internal/httpd/compress.go

v1.40.1
gitbay/internal/httpd/compress.go history · blame · raw

109 lines · 2953 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}
 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 }