1
1
package gzip
2
2
3
3
import (
4
+ "bytes"
4
5
"compress/gzip"
6
+ "context"
5
7
"io/ioutil"
6
8
"net/http"
7
9
"strings"
8
10
"sync"
9
11
10
- "context"
11
-
12
12
"github.com/goadesign/goa"
13
13
)
14
14
@@ -28,28 +28,226 @@ const (
28
28
// capabilities.
29
29
type gzipResponseWriter struct {
30
30
http.ResponseWriter
31
- gzw * gzip.Writer
31
+ gzw * gzip.Writer
32
+ buf * bytes.Buffer
33
+ pool * sync.Pool
34
+ statusCode int
35
+ shouldCompress * bool
36
+ o options
32
37
}
33
38
34
39
// Write writes bytes to the gzip.Writer. It will also set the Content-Type
35
40
// header using the net/http library content type detection if the Content-Type
36
41
// header was not set yet.
37
- func (grw gzipResponseWriter ) Write (b []byte ) (int , error ) {
42
+ func (grw * gzipResponseWriter ) Write (b []byte ) (int , error ) {
38
43
if len (grw .Header ().Get (headerContentType )) == 0 {
39
44
grw .Header ().Set (headerContentType , http .DetectContentType (b ))
40
45
}
41
- return grw .gzw .Write (b )
46
+
47
+ // If we already decided to gzip, do that.
48
+ if grw .gzw != nil {
49
+ return grw .gzw .Write (b )
50
+ }
51
+
52
+ // If we have already decided not to gzip, do that.
53
+ if grw .shouldCompress != nil && ! * grw .shouldCompress {
54
+ return grw .ResponseWriter .Write (b )
55
+ }
56
+
57
+ // Detect types, check status code.
58
+ if grw .shouldCompress == nil {
59
+ s := grw .o .shouldCompress (grw .Header ().Get (headerContentType ), grw .statusCode )
60
+ grw .shouldCompress = & s
61
+ if ! s {
62
+ return grw .ResponseWriter .Write (b )
63
+ }
64
+ }
65
+
66
+ // Check if length is above minimum,
67
+ // if not save to buffer.
68
+ size := len (b ) + grw .buf .Len ()
69
+ if size < grw .o .minSize {
70
+ return grw .buf .Write (b )
71
+ }
72
+
73
+ // Reset our gzip writer to use the http.ResponseWriter
74
+ // Retrieve gzip writer from the pool. Reset it to use the ResponseWriter.
75
+ // This allows us to re-use an already allocated buffer rather than
76
+ // allocating a new buffer for every request.
77
+ grw .Header ().Set (headerContentEncoding , encodingGzip )
78
+ grw .Header ().Set (headerVary , headerAcceptEncoding )
79
+
80
+ gz := grw .pool .Get ().(* gzip.Writer )
81
+ gz .Reset (grw .ResponseWriter )
82
+ grw .gzw = gz
83
+
84
+ // Write buffer
85
+ if grw .buf .Len () > 0 {
86
+ _ , err := gz .Write (grw .buf .Bytes ())
87
+ if err != nil {
88
+ return 0 , err
89
+ }
90
+ grw .buf .Reset ()
91
+ }
92
+ return gz .Write (b )
93
+ }
94
+
95
+ func (grw * gzipResponseWriter ) WriteHeader (n int ) {
96
+ grw .statusCode = n
97
+ grw .ResponseWriter .WriteHeader (n )
98
+ }
99
+
100
+ type (
101
+ // Option allows to override default parameters.
102
+ Option func (* options ) error
103
+
104
+ // options contains final options
105
+ options struct {
106
+ minSize int
107
+ contentTypes []string
108
+ statusCodes map [int ]struct {}
109
+ }
110
+ )
111
+
112
+ var defaultOptions = options {
113
+ minSize : 256 ,
114
+ contentTypes : defaultContentTypes ,
115
+ }
116
+
117
+ func init () {
118
+ defaultOptions .statusCodes = make (map [int ]struct {}, len (defaultStatusCodes ))
119
+ for _ , v := range defaultStatusCodes {
120
+ defaultOptions .statusCodes [v ] = struct {}{}
121
+ }
122
+ }
123
+
124
+ // defaultContentTypes is the default list of content types for which
125
+ // a Handler considers gzip compression. This list originates from the
126
+ // file compression.conf within the Apache configuration found at
127
+ // https://html5boilerplate.com/
128
+ var defaultContentTypes = []string {
129
+ "application/atom+xml" ,
130
+ "application/font-sfnt" ,
131
+ "application/javascript" ,
132
+ "application/json" ,
133
+ "application/ld+json" ,
134
+ "application/manifest+json" ,
135
+ "application/rdf+xml" ,
136
+ "application/rss+xml" ,
137
+ "application/schema+json" ,
138
+ "application/vnd." , // All custom vendor types
139
+ "application/x-font-ttf" ,
140
+ "application/x-javascript" ,
141
+ "application/x-web-app-manifest+json" ,
142
+ "application/xhtml+xml" ,
143
+ "application/xml" ,
144
+ "font/eot" ,
145
+ "font/opentype" ,
146
+ "image/bmp" ,
147
+ "image/svg+xml" ,
148
+ "image/vnd.microsoft.icon" ,
149
+ "image/x-icon" ,
150
+ "text/cache-manifest" ,
151
+ "text/css" ,
152
+ "text/html" ,
153
+ "text/javascript" ,
154
+ "text/plain" ,
155
+ "text/vcard" ,
156
+ "text/vnd.rim.location.xloc" ,
157
+ "text/vtt" ,
158
+ "text/x-component" ,
159
+ "text/x-cross-domain-policy" ,
160
+ "text/xml" ,
161
+ }
162
+
163
+ // defaultStatusCodes are the status codes that will be compressed.
164
+ var defaultStatusCodes = []int {
165
+ http .StatusOK ,
166
+ http .StatusCreated ,
167
+ http .StatusAccepted ,
168
+ }
169
+
170
+ // AddContentTypes allows to specify specific content types to encode.
171
+ // Adds to previous content types.
172
+ func AddContentTypes (types ... string ) Option {
173
+ return func (c * options ) error {
174
+ dst := make ([]string , len (c .contentTypes )+ len (types ))
175
+ copy (dst , c .contentTypes )
176
+ copy (dst [len (c .contentTypes ):], types )
177
+ c .contentTypes = dst
178
+ return nil
179
+ }
42
180
}
43
181
44
- // handler struct contains the ServeHTTP method
45
- type handler struct {
46
- pool sync.Pool
182
+ // OnlyContentTypes allows to specify specific content types to encode.
183
+ // Overrides previous content types.
184
+ // no types = ignore content types (always compress).
185
+ func OnlyContentTypes (types ... string ) Option {
186
+ return func (c * options ) error {
187
+ if len (types ) == 0 {
188
+ c .contentTypes = nil
189
+ return nil
190
+ }
191
+ c .contentTypes = types
192
+ return nil
193
+ }
194
+ }
195
+
196
+ // AddStatusCodes allows to specify specific content types to encode.
197
+ // All content types that has the supplied prefixes are compressed.
198
+ func AddStatusCodes (codes ... int ) Option {
199
+ return func (c * options ) error {
200
+ dst := make (map [int ]struct {}, len (c .statusCodes )+ len (codes ))
201
+ for code := range c .statusCodes {
202
+ dst [code ] = struct {}{}
203
+ }
204
+ for _ , code := range codes {
205
+ c .statusCodes [code ] = struct {}{}
206
+ }
207
+ return nil
208
+ }
209
+ }
210
+
211
+ // OnlyStatusCodes allows to specify specific content types to encode.
212
+ // All content types that has the supplied prefixes are compressed.
213
+ // No codes = ignore content types (always compress).
214
+ func OnlyStatusCodes (codes ... int ) Option {
215
+ return func (c * options ) error {
216
+ if len (codes ) == 0 {
217
+ c .statusCodes = nil
218
+ return nil
219
+ }
220
+ c .statusCodes = make (map [int ]struct {}, len (codes ))
221
+ for _ , code := range codes {
222
+ c .statusCodes [code ] = struct {}{}
223
+ }
224
+ return nil
225
+ }
226
+ }
227
+
228
+ // MinSize will set a minimum size for compression.
229
+ func MinSize (n int ) Option {
230
+ return func (c * options ) error {
231
+ if n <= 0 {
232
+ c .minSize = 0
233
+ return nil
234
+ }
235
+ c .minSize = n
236
+ return nil
237
+ }
47
238
}
48
239
49
240
// Middleware encodes the response using Gzip encoding and sets all the appropriate
50
241
// headers. If the Content-Type is not set, it will be set by calling
51
242
// http.DetectContentType on the data being written.
52
- func Middleware (level int ) goa.Middleware {
243
+ func Middleware (level int , o ... Option ) goa.Middleware {
244
+ opts := defaultOptions
245
+ for _ , opt := range o {
246
+ err := opt (& opts )
247
+ if err != nil {
248
+ panic (err )
249
+ }
250
+ }
53
251
gzipPool := sync.Pool {
54
252
New : func () interface {} {
55
253
gz , err := gzip .NewWriterLevel (ioutil .Discard , level )
@@ -71,23 +269,17 @@ func Middleware(level int) goa.Middleware {
71
269
72
270
// Set the appropriate gzip headers.
73
271
resp := goa .ContextResponse (ctx )
74
- resp .Header ().Set (headerContentEncoding , encodingGzip )
75
- resp .Header ().Set (headerVary , headerAcceptEncoding )
76
-
77
- // Retrieve gzip writer from the pool. Reset it to use the ResponseWriter.
78
- // This allows us to re-use an already allocated buffer rather than
79
- // allocating a new buffer for every request.
80
- gz := gzipPool .Get ().(* gzip.Writer )
81
272
82
273
// Get the original http.ResponseWriter
83
274
w := resp .SwitchWriter (nil )
84
- // Reset our gzip writer to use the http.ResponseWriter
85
- gz .Reset (w )
86
275
87
276
// Wrap the original http.ResponseWriter with our gzipResponseWriter
88
- grw := gzipResponseWriter {
277
+ grw := & gzipResponseWriter {
89
278
ResponseWriter : w ,
90
- gzw : gz ,
279
+ pool : & gzipPool ,
280
+ buf : bytes .NewBuffer (nil ),
281
+ statusCode : http .StatusOK ,
282
+ o : opts ,
91
283
}
92
284
93
285
// Set the new http.ResponseWriter
@@ -102,9 +294,49 @@ func Middleware(level int) goa.Middleware {
102
294
103
295
// Delete the content length after we know we have been written to.
104
296
grw .Header ().Del (headerContentLength )
105
- gz .Close ()
106
- gzipPool .Put (gz )
297
+ if grw .buf .Len () > 0 {
298
+ _ , err = grw .ResponseWriter .Write (grw .buf .Bytes ())
299
+ if err != nil {
300
+ return err
301
+ }
302
+ }
303
+
304
+ // Flush+recycle gzip writer
305
+ if grw .gzw != nil {
306
+ err := grw .gzw .Close ()
307
+ if err != nil {
308
+ return err
309
+ }
310
+ gzipPool .Put (grw .gzw )
311
+ }
107
312
return
108
313
}
109
314
}
110
315
}
316
+
317
+ // returns true if we've been configured to compress the specific content type.
318
+ func (o options ) shouldCompress (contentType string , statusCode int ) bool {
319
+ // If contentTypes is nil we handle all content types.
320
+ if len (o .contentTypes ) > 0 {
321
+ ct := strings .ToLower (contentType )
322
+ ct = strings .Split (ct , ";" )[0 ]
323
+ found := false
324
+ for _ , v := range o .contentTypes {
325
+ if strings .HasPrefix (ct , v ) {
326
+ found = true
327
+ break
328
+ }
329
+ }
330
+ if ! found {
331
+ return false
332
+ }
333
+ }
334
+ if len (o .statusCodes ) > 0 {
335
+ _ , ok := o .statusCodes [statusCode ]
336
+ if ! ok {
337
+ return false
338
+ }
339
+ }
340
+
341
+ return true
342
+ }
0 commit comments