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  }