-
Notifications
You must be signed in to change notification settings - Fork 97
Expand file tree
/
Copy pathgzip.go
More file actions
174 lines (151 loc) · 5.74 KB
/
gzip.go
File metadata and controls
174 lines (151 loc) · 5.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
package gzip
import (
"bufio"
"bytes"
"compress/gzip"
"errors"
"net"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
const (
BestCompression = gzip.BestCompression
BestSpeed = gzip.BestSpeed
DefaultCompression = gzip.DefaultCompression
NoCompression = gzip.NoCompression
HuffmanOnly = gzip.HuffmanOnly
gzipEncoding = "gzip"
)
func Gzip(level int, options ...Option) gin.HandlerFunc {
return newGzipHandler(level, options...).Handle
}
type gzipWriter struct {
gin.ResponseWriter
writer *gzip.Writer
statusWritten bool
status int
// minLength is the minimum length of the response body (in bytes) to enable compression
minLength int
// shouldCompress indicates whether the minimum length for compression has been met
shouldCompress bool
// buffer to store response data in case minimum length for compression wasn't met
buffer bytes.Buffer
}
func (g *gzipWriter) WriteString(s string) (int, error) {
return g.Write([]byte(s))
}
// Write writes the given data to the appropriate underlying writer.
// Note that this method can be called multiple times within a single request.
func (g *gzipWriter) Write(data []byte) (int, error) {
// Check status from ResponseWriter if not set via WriteHeader
if !g.statusWritten {
g.status = g.ResponseWriter.Status()
}
// For error responses (4xx, 5xx), don't compress
// Always check the current status, even if WriteHeader was called
if g.status >= 400 {
g.removeGzipHeaders()
return g.ResponseWriter.Write(data)
}
// Check if response is already gzip-compressed by looking at Content-Encoding header
// If upstream handler already set gzip encoding, pass through without double compression
if contentEncoding := g.Header().Get("Content-Encoding"); contentEncoding != "" && contentEncoding != gzipEncoding {
// Different encoding, remove our gzip headers and pass through
g.removeGzipHeaders()
return g.ResponseWriter.Write(data)
} else if contentEncoding == "gzip" {
// Already gzip encoded by upstream, check if this looks like gzip data
if len(data) >= 2 && data[0] == 0x1f && data[1] == 0x8b {
// This is already gzip data, remove our headers and pass through
g.removeGzipHeaders()
return g.ResponseWriter.Write(data)
}
}
// Now handle dynamic gzipping based on the client's specified minimum length
// (if no min length specified, all responses get gzipped)
// If a Content-Length header is set, use that to decide whether to compress so that we don't need to buffer
if g.Header().Get("Content-Length") != "" {
// invalid header treated the same as having no Content-Length
contentLen, err := strconv.Atoi(g.Header().Get("Content-Length"))
if err == nil {
if contentLen < g.minLength {
return g.ResponseWriter.Write(data)
}
g.shouldCompress = true
g.Header().Del("Content-Length")
}
}
// Handle buffering here if Content-Length value couldn't tell us whether to gzip
//
// Check if the response body is large enough to be compressed.
// - If so, skip this condition and proceed with the normal write process.
// - If not, store the data in the buffer (in case more data is written in future Write calls).
// (At the end, if the response body is still too small, the caller should check shouldCompress and
// use the data stored in the buffer to write the response instead.)
if !g.shouldCompress && len(data) >= g.minLength {
g.shouldCompress = true
} else if !g.shouldCompress {
lenWritten, err := g.buffer.Write(data)
if err != nil || g.buffer.Len() < g.minLength {
return lenWritten, err
}
g.shouldCompress = true
data = g.buffer.Bytes()
}
return g.writer.Write(data)
}
// Status returns the HTTP response status code
func (g *gzipWriter) Status() int {
if g.statusWritten {
return g.status
}
return g.ResponseWriter.Status()
}
// Size returns the number of bytes already written into the response http body
func (g *gzipWriter) Size() int {
return g.ResponseWriter.Size()
}
// Written returns true if the response body was already written
func (g *gzipWriter) Written() bool {
return g.ResponseWriter.Written()
}
// WriteHeaderNow forces to write the http header
func (g *gzipWriter) WriteHeaderNow() {
g.ResponseWriter.WriteHeaderNow()
}
// removeGzipHeaders removes compression-related headers for error responses
func (g *gzipWriter) removeGzipHeaders() {
g.Header().Del("Content-Encoding")
g.Header().Del("Vary")
g.Header().Del("ETag")
}
func (g *gzipWriter) Flush() {
_ = g.writer.Flush()
g.ResponseWriter.Flush()
}
// Fix: https://github.com/mholt/caddy/issues/38
func (g *gzipWriter) WriteHeader(code int) {
g.status = code
g.statusWritten = true
// Don't remove gzip headers immediately for error responses in WriteHeader
// because some handlers (like static file server) may call WriteHeader multiple times
// We'll check the status in Write() method when content is actually written
g.ResponseWriter.WriteHeader(code)
}
// Ensure gzipWriter implements the http.Hijacker interface.
// This will cause a compile-time error if gzipWriter does not implement all methods of the http.Hijacker interface.
var _ http.Hijacker = (*gzipWriter)(nil)
// Hijack allows the caller to take over the connection from the HTTP server.
// After a call to Hijack, the HTTP server library will not do anything else with the connection.
// It becomes the caller's responsibility to manage and close the connection.
//
// It returns the underlying net.Conn, a buffered reader/writer for the connection, and an error
// if the ResponseWriter does not support the Hijacker interface.
func (g *gzipWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
hijacker, ok := g.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, errors.New("the ResponseWriter doesn't support the Hijacker interface")
}
return hijacker.Hijack()
}