Skip to content

Commit 2e7eddd

Browse files
committed
Configure Content-Length check to avoid buffering completely
1 parent 56dc982 commit 2e7eddd

File tree

3 files changed

+42
-10
lines changed

3 files changed

+42
-10
lines changed

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ curl -i --compressed 'http://localhost:8080/ping?size=2047'
8585
curl -i --compressed 'http://localhost:8080/ping?size=2048'
8686
```
8787

88+
Notes:
89+
- If a "Content-Length" header is set, that will be used to determine whether to compress based on the given min length.
90+
- If no "Content-Length" header is set, a buffer is used to temporarily store writes until the min length is met or the request completes.
91+
- Setting a high min length will result in more buffering (2048 bytes is a recommended default for most cases)
92+
- The handler performs optimizations to avoid unnecessary operations, such as testing if `len(data)` exceeds min length before writing to the buffer, and reusing buffers between requests.
93+
8894
### Customized Excluded Extensions
8995

9096
```go

gzip.go

+14-8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"errors"
88
"net"
99
"net/http"
10+
"strconv"
1011

1112
"github.com/gin-gonic/gin"
1213
)
@@ -35,12 +36,23 @@ type gzipWriter struct {
3536
}
3637

3738
func (g *gzipWriter) WriteString(s string) (int, error) {
38-
g.Header().Del("Content-Length")
3939
return g.Write([]byte(s))
4040
}
4141

42+
// Write writes the given data to the appropriate underlying writer.
43+
// Note that this method can be called multiple times within a single request.
4244
func (g *gzipWriter) Write(data []byte) (int, error) {
43-
g.Header().Del("Content-Length")
45+
// If a Content-Length header is set, use that to decide whether to compress the response.
46+
if g.Header().Get("Content-Length") != "" {
47+
contentLen, _ := strconv.Atoi(g.Header().Get("Content-Length"))
48+
if contentLen >= g.minLength {
49+
g.wasMinLengthMetForCompression = true
50+
} else {
51+
return g.ResponseWriter.Write(data)
52+
}
53+
g.Header().Del("Content-Length")
54+
}
55+
4456
// Check if the response body is large enough to be compressed. If so, skip this condition and proceed with the
4557
// normal write process. If not, store the data in the buffer in case more data is written later.
4658
// (At the end, if the response body is still too small, the caller should check wasMinLengthMetForCompression and
@@ -64,12 +76,6 @@ func (g *gzipWriter) Flush() {
6476
g.ResponseWriter.Flush()
6577
}
6678

67-
// Fix: https://github.com/mholt/caddy/issues/38
68-
func (g *gzipWriter) WriteHeader(code int) {
69-
g.Header().Del("Content-Length")
70-
g.ResponseWriter.WriteHeader(code)
71-
}
72-
7379
// Ensure gzipWriter implements the http.Hijacker interface.
7480
// This will cause a compile-time error if gzipWriter does not implement all methods of the http.Hijacker interface.
7581
var _ http.Hijacker = (*gzipWriter)(nil)

gzip_test.go

+22-2
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,6 @@ func TestMinLengthShortResponse(t *testing.T) {
385385
router := gin.New()
386386
router.Use(Gzip(DefaultCompression, WithMinLength(2048)))
387387
router.GET("/", func(c *gin.Context) {
388-
c.Header("Content-Length", strconv.Itoa(len(testResponse)))
389388
c.String(200, testResponse)
390389
})
391390

@@ -405,7 +404,6 @@ func TestMinLengthLongResponse(t *testing.T) {
405404
router := gin.New()
406405
router.Use(Gzip(DefaultCompression, WithMinLength(2048)))
407406
router.GET("/", func(c *gin.Context) {
408-
c.Header("Content-Length", "2048")
409407
c.String(200, strings.Repeat("a", 2048))
410408
})
411409

@@ -439,6 +437,28 @@ func TestMinLengthMultiWriteResponse(t *testing.T) {
439437
assert.Less(t, w.Body.Len(), 2048)
440438
}
441439

440+
// Note this test intentionally triggers gzipping even when the actual response doesn't meet min length. This is because
441+
// we use the Content-Length header as the primary determinant of compression to avoid the cost of buffering.
442+
func TestMinLengthUsesContentLengthHeaderInsteadOfBuffering(t *testing.T) {
443+
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil)
444+
req.Header.Add(headerAcceptEncoding, "gzip")
445+
446+
router := gin.New()
447+
router.Use(Gzip(DefaultCompression, WithMinLength(2048)))
448+
router.GET("/", func(c *gin.Context) {
449+
c.Header("Content-Length", "2048")
450+
c.String(200, testResponse)
451+
})
452+
453+
w := httptest.NewRecorder()
454+
router.ServeHTTP(w, req)
455+
456+
assert.Equal(t, 200, w.Code)
457+
assert.Equal(t, "gzip", w.Header().Get(headerContentEncoding))
458+
assert.NotEmpty(t, w.Header().Get("Content-Length"))
459+
assert.NotEqual(t, "19", w.Header().Get("Content-Length"))
460+
}
461+
442462
type hijackableResponse struct {
443463
Hijacked bool
444464
header http.Header

0 commit comments

Comments
 (0)