goyave.dev/goyave/v5@v5.0.0-rc9.0.20240517145003-d3f977d0b9f3/middleware/compress/compress.go (about) 1 package compress 2 3 import ( 4 "compress/gzip" 5 "io" 6 "net/http" 7 8 "github.com/samber/lo" 9 "goyave.dev/goyave/v5" 10 "goyave.dev/goyave/v5/util/errors" 11 "goyave.dev/goyave/v5/util/httputil" 12 ) 13 14 // Encoder is an interface that wraps the methods returning the information 15 // necessary for the compress middleware to work. 16 // 17 // `NewWriter` returns any `io.WriteCloser`, allowing the middleware to support 18 // any compression algorithm. 19 // 20 // `Encoding` returns the name of the compression algorithm. Using the returned value, 21 // the middleware: 22 // 1. detects the client's preferred encoding with the `Accept-Encoding` request header 23 // 2. replaces the response writer with the writer returned by `NewWriter` 24 // 3. sets the `Content-Encoding` response header 25 type Encoder interface { 26 NewWriter(io.Writer) io.WriteCloser 27 Encoding() string 28 } 29 30 type compressWriter struct { 31 io.WriteCloser 32 http.ResponseWriter 33 childWriter io.Writer 34 } 35 36 func (w *compressWriter) PreWrite(b []byte) { 37 if pr, ok := w.childWriter.(goyave.PreWriter); ok { 38 pr.PreWrite(b) 39 } 40 h := w.ResponseWriter.Header() 41 if h.Get("Content-Type") == "" { 42 h.Set("Content-Type", http.DetectContentType(b)) 43 } 44 h.Del("Content-Length") 45 } 46 47 func (w *compressWriter) Write(b []byte) (int, error) { 48 n, err := w.WriteCloser.Write(b) 49 return n, errors.New(err) 50 } 51 52 func (w *compressWriter) Close() error { 53 err := errors.New(w.WriteCloser.Close()) 54 55 if wr, ok := w.childWriter.(io.Closer); ok { 56 return errors.New(wr.Close()) 57 } 58 59 return err 60 } 61 62 // Gzip encoder for the gzip format using Go's standard `compress/gzip` package. 63 // 64 // Takes a compression level as parameter. Accepted values are defined by constants 65 // in the standard `compress/gzip` package. 66 type Gzip struct { 67 Level int 68 } 69 70 // Encoding returns "gzip". 71 func (w *Gzip) Encoding() string { 72 return "gzip" 73 } 74 75 // NewWriter returns a new `compress/gzip.Writer` using the compression level 76 // defined in this Gzip encoder. 77 func (w *Gzip) NewWriter(wr io.Writer) io.WriteCloser { 78 writer, err := gzip.NewWriterLevel(wr, w.Level) 79 if err != nil { 80 panic(errors.New(err)) 81 } 82 return writer 83 } 84 85 // Middleware compresses HTTP responses. 86 // 87 // This middleware supports multiple algorithms thanks to the `Encoders` slice. 88 // The encoder will be chosen depending on the request's `Accept-Encoding` header, 89 // and the value returned by the `Encoder`'s `Encoding()` method. Quality values in 90 // the headers are taken into account. 91 // 92 // If the header's value is `*`, the first element of the slice is used. 93 // If none of the accepted encodings are available in the `Encoders` slice, then the 94 // response will not be compressed and the middleware immediately passes. 95 // 96 // If the middleware successfully replaces the response writer, the `Accept-Encoding` 97 // header is removed from the request to avoid potential clashes with potential other 98 // encoding middleware. 99 // 100 // If not set at the first call of `Write()`, the middleware will automatically detect 101 // and set the `Content-Type` header using `http.DetectContentType()`. 102 // 103 // The middleware ignores hijacked responses or requests containing the `Upgrade` header. 104 // 105 // **Example:** 106 // 107 // compressMiddleware := &compress.Middleware{ 108 // Encoders: []compress.Encoder{ 109 // &compress.Gzip{Level: gzip.BestCompression}, 110 // }, 111 // } 112 type Middleware struct { 113 goyave.Component 114 Encoders []Encoder 115 } 116 117 // Handle implementation of `goyave.Middleware`. 118 func (m *Middleware) Handle(next goyave.Handler) goyave.Handler { 119 return func(response *goyave.Response, request *goyave.Request) { 120 encoder := m.getEncoder(response, request) 121 if encoder == nil { 122 next(response, request) 123 return 124 } 125 126 request.Header().Del("Accept-Encoding") 127 128 respWriter := response.Writer() 129 compressWriter := &compressWriter{ 130 WriteCloser: encoder.NewWriter(respWriter), 131 ResponseWriter: response, 132 childWriter: respWriter, 133 } 134 response.SetWriter(compressWriter) 135 response.Header().Set("Content-Encoding", encoder.Encoding()) 136 137 next(response, request) 138 } 139 } 140 141 func (m *Middleware) getEncoder(response *goyave.Response, request *goyave.Request) Encoder { 142 if response.Hijacked() || request.Header().Get("Upgrade") != "" { 143 return nil 144 } 145 encodings := httputil.ParseMultiValuesHeader(request.Header().Get("Accept-Encoding")) 146 for _, h := range encodings { 147 if h.Value == "*" { 148 return m.Encoders[0] 149 } 150 w, ok := lo.Find(m.Encoders, func(w Encoder) bool { 151 return w.Encoding() == h.Value 152 }) 153 if ok { 154 return w 155 } 156 } 157 158 return nil 159 }