Skip to content

Commit 872d897

Browse files
klauspostraphael
authored andcommitted
Improve gzip middleware (goadesign#1368)
* Improve gzip middleware Change gzip middleware to only be employed if: * Response is above specified size (Default: 256 bytes) * Status code is one of. (Default: OK, Created, Accepted) * Content type is one of. (Default: Common types+ all `application/vnd.`) Options added are `AddContentTypes`, "OnlyContentTypes", "AddStatusCodes", "OnlyStatusCodes" and "MinSize". Expanded tests to cover these. * Export option type for linter. * Comment wording.
1 parent 3b83d28 commit 872d897

File tree

2 files changed

+507
-23
lines changed

2 files changed

+507
-23
lines changed

middleware/gzip/middleware.go

+254-22
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
package gzip
22

33
import (
4+
"bytes"
45
"compress/gzip"
6+
"context"
57
"io/ioutil"
68
"net/http"
79
"strings"
810
"sync"
911

10-
"context"
11-
1212
"github.com/goadesign/goa"
1313
)
1414

@@ -28,28 +28,226 @@ const (
2828
// capabilities.
2929
type gzipResponseWriter struct {
3030
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
3237
}
3338

3439
// Write writes bytes to the gzip.Writer. It will also set the Content-Type
3540
// header using the net/http library content type detection if the Content-Type
3641
// header was not set yet.
37-
func (grw gzipResponseWriter) Write(b []byte) (int, error) {
42+
func (grw *gzipResponseWriter) Write(b []byte) (int, error) {
3843
if len(grw.Header().Get(headerContentType)) == 0 {
3944
grw.Header().Set(headerContentType, http.DetectContentType(b))
4045
}
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+
}
42180
}
43181

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+
}
47238
}
48239

49240
// Middleware encodes the response using Gzip encoding and sets all the appropriate
50241
// headers. If the Content-Type is not set, it will be set by calling
51242
// 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+
}
53251
gzipPool := sync.Pool{
54252
New: func() interface{} {
55253
gz, err := gzip.NewWriterLevel(ioutil.Discard, level)
@@ -71,23 +269,17 @@ func Middleware(level int) goa.Middleware {
71269

72270
// Set the appropriate gzip headers.
73271
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)
81272

82273
// Get the original http.ResponseWriter
83274
w := resp.SwitchWriter(nil)
84-
// Reset our gzip writer to use the http.ResponseWriter
85-
gz.Reset(w)
86275

87276
// Wrap the original http.ResponseWriter with our gzipResponseWriter
88-
grw := gzipResponseWriter{
277+
grw := &gzipResponseWriter{
89278
ResponseWriter: w,
90-
gzw: gz,
279+
pool: &gzipPool,
280+
buf: bytes.NewBuffer(nil),
281+
statusCode: http.StatusOK,
282+
o: opts,
91283
}
92284

93285
// Set the new http.ResponseWriter
@@ -102,9 +294,49 @@ func Middleware(level int) goa.Middleware {
102294

103295
// Delete the content length after we know we have been written to.
104296
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+
}
107312
return
108313
}
109314
}
110315
}
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

Comments
 (0)