|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 |
- package handlers
-
- import (
- "compress/gzip"
- "io"
- "net/http"
- )
-
- // Thanks to Andrew Gerrand for inspiration:
- // https://groups.google.com/d/msg/golang-nuts/eVnTcMwNVjM/4vYU8id9Q2UJ
- //
- // Also, node's Connect library implementation of the compress middleware:
- // https://github.com/senchalabs/connect/blob/master/lib/middleware/compress.js
- //
- // And StackOverflow's explanation of Vary: Accept-Encoding header:
- // http://stackoverflow.com/questions/7848796/what-does-varyaccept-encoding-mean
-
- // Internal gzipped writer that satisfies both the (body) writer in gzipped format,
- // and maintains the rest of the ResponseWriter interface for header manipulation.
- type gzipResponseWriter struct {
- io.Writer
- http.ResponseWriter
- r *http.Request // Keep a hold of the Request, for the filter function
- filtered bool // Has the request been run through the filter function?
- dogzip bool // Should we do GZIP compression for this request?
- filterFn func(http.ResponseWriter, *http.Request) bool
- }
-
- // Make sure the filter function is applied.
- func (w *gzipResponseWriter) applyFilter() {
- if !w.filtered {
- if w.dogzip = w.filterFn(w, w.r); w.dogzip {
- setGzipHeaders(w.Header())
- }
- w.filtered = true
- }
- }
-
- // Unambiguous Write() implementation (otherwise both ResponseWriter and Writer
- // want to claim this method).
- func (w *gzipResponseWriter) Write(b []byte) (int, error) {
- w.applyFilter()
- if w.dogzip {
- // Write compressed
- return w.Writer.Write(b)
- }
- // Write uncompressed
- return w.ResponseWriter.Write(b)
- }
-
- // Intercept the WriteHeader call to correctly set the GZIP headers.
- func (w *gzipResponseWriter) WriteHeader(code int) {
- w.applyFilter()
- w.ResponseWriter.WriteHeader(code)
- }
-
- // Implement WrapWriter interface
- func (w *gzipResponseWriter) WrappedWriter() http.ResponseWriter {
- return w.ResponseWriter
- }
-
- var (
- defaultFilterTypes = [...]string{
- "text",
- "javascript",
- "json",
- }
- )
-
- // Default filter to check if the response should be GZIPped.
- // By default, all text (html, css, xml, ...), javascript and json
- // content types are candidates for GZIP.
- func defaultFilter(w http.ResponseWriter, r *http.Request) bool {
- hdr := w.Header()
- for _, tp := range defaultFilterTypes {
- ok := HeaderMatch(hdr, "Content-Type", HmContains, tp)
- if ok {
- return true
- }
- }
- return false
- }
-
- // GZIPHandlerFunc is the same as GZIPHandler, it is just a convenience
- // signature that accepts a func(http.ResponseWriter, *http.Request) instead of
- // a http.Handler interface. It saves the boilerplate http.HandlerFunc() cast.
- func GZIPHandlerFunc(h http.HandlerFunc, filterFn func(http.ResponseWriter, *http.Request) bool) http.HandlerFunc {
- return GZIPHandler(h, filterFn)
- }
-
- // Gzip compression HTTP handler. If the client supports it, it compresses the response
- // written by the wrapped handler. The filter function is called when the response is about
- // to be written to determine if compression should be applied. If this argument is nil,
- // the default filter will GZIP only content types containing /json|text|javascript/.
- func GZIPHandler(h http.Handler, filterFn func(http.ResponseWriter, *http.Request) bool) http.HandlerFunc {
- if filterFn == nil {
- filterFn = defaultFilter
- }
- return func(w http.ResponseWriter, r *http.Request) {
- if _, ok := getGzipWriter(w); ok {
- // Self-awareness, gzip handler is already set up
- h.ServeHTTP(w, r)
- return
- }
- hdr := w.Header()
- setVaryHeader(hdr)
-
- // Do nothing on a HEAD request
- if r.Method == "HEAD" {
- h.ServeHTTP(w, r)
- return
- }
- if !acceptsGzip(r.Header) {
- // No gzip support from the client, return uncompressed
- h.ServeHTTP(w, r)
- return
- }
-
- // Prepare a gzip response container
- gz := gzip.NewWriter(w)
- gzw := &gzipResponseWriter{
- Writer: gz,
- ResponseWriter: w,
- r: r,
- filterFn: filterFn,
- }
- h.ServeHTTP(gzw, r)
- // Iff the handler completed successfully (no panic) and GZIP was indeed used, close the gzip writer,
- // which seems to generate a Write to the underlying writer.
- if gzw.dogzip {
- gz.Close()
- }
- }
- }
-
- // Add the vary by "accept-encoding" header if it is not already set.
- func setVaryHeader(hdr http.Header) {
- if !HeaderMatch(hdr, "Vary", HmContains, "accept-encoding") {
- hdr.Add("Vary", "Accept-Encoding")
- }
- }
-
- // Checks if the client accepts GZIP-encoded responses.
- func acceptsGzip(hdr http.Header) bool {
- ok := HeaderMatch(hdr, "Accept-Encoding", HmContains, "gzip")
- if !ok {
- ok = HeaderMatch(hdr, "Accept-Encoding", HmEquals, "*")
- }
- return ok
- }
-
- func setGzipHeaders(hdr http.Header) {
- // The content-type will be explicitly set somewhere down the path of handlers
- hdr.Set("Content-Encoding", "gzip")
- hdr.Del("Content-Length")
- }
-
- // Helper function to retrieve the gzip writer.
- func getGzipWriter(w http.ResponseWriter) (*gzipResponseWriter, bool) {
- gz, ok := GetResponseWriter(w, func(tst http.ResponseWriter) bool {
- _, ok := tst.(*gzipResponseWriter)
- return ok
- })
- if ok {
- return gz.(*gzipResponseWriter), true
- }
- return nil, false
- }
|