Skip to content

Commit ff4cafa

Browse files
authored
fix(header): static file error gzip header handling and add tests (#123)
* fix: fix static file error gzip header handling and add tests - Delay removal of gzip headers for error responses until Write is called, ensuring headers are set correctly for static files. - Remove gzip headers for error responses after the handler finishes, improving behavior for static file serving. - Add comprehensive tests covering static file compression, uncompressed responses, 404 error handling, and directory listing. - Add a bug-repro test demonstrating that static file gzip headers may be incorrectly removed due to how status codes are handled. fix #122 Signed-off-by: appleboy <[email protected]> * test: improve test maintainability and security through refactoring - Replace hardcoded "gzip" string with a named constant for improved maintainability in tests - Change test file permissions in static file tests from octal 0644 to 0o600 for better security, except for one case using 0o644 - Minor formatting improvements to string concatenation in test content Signed-off-by: appleboy <[email protected]> * fix: set secure file permissions for generated test.js - Change file permission from 644 to 600 when writing test.js in the static file test Signed-off-by: appleboy <[email protected]> --------- Signed-off-by: appleboy <[email protected]>
1 parent 218712e commit ff4cafa

File tree

4 files changed

+205
-10
lines changed

4 files changed

+205
-10
lines changed

gzip.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func (g *gzipWriter) Write(data []byte) (int, error) {
4242
}
4343

4444
// For error responses (4xx, 5xx), don't compress
45+
// Always check the current status, even if WriteHeader was called
4546
if g.status >= 400 {
4647
g.removeGzipHeaders()
4748
return g.ResponseWriter.Write(data)
@@ -90,10 +91,9 @@ func (g *gzipWriter) WriteHeader(code int) {
9091
g.status = code
9192
g.statusWritten = true
9293

93-
// Remove gzip headers for error responses
94-
if code >= 400 {
95-
g.removeGzipHeaders()
96-
}
94+
// Don't remove gzip headers immediately for error responses in WriteHeader
95+
// because some handlers (like static file server) may call WriteHeader multiple times
96+
// We'll check the status in Write() method when content is actually written
9797

9898
g.Header().Del("Content-Length")
9999
g.ResponseWriter.WriteHeader(code)

handler.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ func (g *gzipHandler) Handle(c *gin.Context) {
8989
defer func() {
9090
// Only close gzip writer if it was actually used (not for error responses)
9191
if gw.status >= 400 {
92+
// Remove gzip headers for error responses when handler is complete
93+
gw.removeGzipHeaders()
9294
gz.Reset(io.Discard)
9395
} else if c.Writer.Size() < 0 {
9496
// do not write gzip footer when nothing is written to the response body

handler_test.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"github.com/stretchr/testify/assert"
1414
)
1515

16+
const gzipEncoding = "gzip"
17+
1618
func TestHandleGzip(t *testing.T) {
1719
gin.SetMode(gin.TestMode)
1820

@@ -26,8 +28,8 @@ func TestHandleGzip(t *testing.T) {
2628
{
2729
name: "Gzip compression",
2830
path: "/",
29-
acceptEncoding: "gzip",
30-
expectedContentEncoding: "gzip",
31+
acceptEncoding: gzipEncoding,
32+
expectedContentEncoding: gzipEncoding,
3133
expectedBody: "Gzip Test Response",
3234
},
3335
{
@@ -56,7 +58,7 @@ func TestHandleGzip(t *testing.T) {
5658
assert.Equal(t, http.StatusOK, w.Code)
5759
assert.Equal(t, tt.expectedContentEncoding, w.Header().Get("Content-Encoding"))
5860

59-
if tt.expectedContentEncoding == "gzip" {
61+
if tt.expectedContentEncoding == gzipEncoding {
6062
gr, err := gzip.NewReader(w.Body)
6163
assert.NoError(t, err)
6264
defer gr.Close()
@@ -91,7 +93,7 @@ func TestHandleDecompressGzip(t *testing.T) {
9193
})
9294

9395
req, _ := http.NewRequestWithContext(context.Background(), "POST", "/", buf)
94-
req.Header.Set("Content-Encoding", "gzip")
96+
req.Header.Set("Content-Encoding", gzipEncoding)
9597

9698
w := httptest.NewRecorder()
9799
router.ServeHTTP(w, req)
@@ -110,7 +112,7 @@ func TestHandle404NoCompression(t *testing.T) {
110112
}{
111113
{
112114
name: "404 with gzip accept-encoding should not compress",
113-
acceptEncoding: "gzip",
115+
acceptEncoding: gzipEncoding,
114116
expectedGzip: false,
115117
},
116118
{
@@ -142,7 +144,7 @@ func TestHandle404NoCompression(t *testing.T) {
142144
// Check that Content-Encoding header is not set for 404 responses
143145
contentEncoding := w.Header().Get("Content-Encoding")
144146
if tt.expectedGzip {
145-
assert.Equal(t, "gzip", contentEncoding)
147+
assert.Equal(t, gzipEncoding, contentEncoding)
146148
} else {
147149
assert.Empty(t, contentEncoding, "404 responses should not have Content-Encoding: gzip")
148150
}

static_test.go

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package gzip
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"os"
8+
"path/filepath"
9+
"testing"
10+
11+
"github.com/gin-gonic/gin"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
func TestStaticFileWithGzip(t *testing.T) {
17+
// Create a temporary directory and file for testing
18+
tmpDir, err := os.MkdirTemp("", "gzip_static_test")
19+
require.NoError(t, err)
20+
defer os.RemoveAll(tmpDir)
21+
22+
// Create a test file
23+
testFile := filepath.Join(tmpDir, "test.txt")
24+
testContent := "This is a test file for static gzip compression testing. " +
25+
"It should be long enough to trigger gzip compression."
26+
err = os.WriteFile(testFile, []byte(testContent), 0o600)
27+
require.NoError(t, err)
28+
29+
// Set up Gin router with gzip middleware and static file serving
30+
gin.SetMode(gin.TestMode)
31+
router := gin.New()
32+
router.Use(Gzip(DefaultCompression))
33+
router.Static("/static", tmpDir)
34+
35+
// Test static file request with gzip support
36+
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/static/test.txt", nil)
37+
req.Header.Add(headerAcceptEncoding, "gzip")
38+
39+
w := httptest.NewRecorder()
40+
router.ServeHTTP(w, req)
41+
42+
// The response should be successful and compressed
43+
assert.Equal(t, http.StatusOK, w.Code)
44+
45+
// This is what should happen but currently fails due to the bug
46+
// The static handler initially sets status to 404, causing gzip headers to be removed
47+
assert.Equal(t, "gzip", w.Header().Get(headerContentEncoding), "Static file should be gzip compressed")
48+
assert.Equal(t, headerAcceptEncoding, w.Header().Get(headerVary), "Vary header should be set")
49+
50+
// The compressed content should be smaller than original
51+
assert.Less(t, w.Body.Len(), len(testContent), "Compressed content should be smaller")
52+
}
53+
54+
func TestStaticFileWithoutGzip(t *testing.T) {
55+
// Create a temporary directory and file for testing
56+
tmpDir, err := os.MkdirTemp("", "gzip_static_test")
57+
require.NoError(t, err)
58+
defer os.RemoveAll(tmpDir)
59+
60+
// Create a test file
61+
testFile := filepath.Join(tmpDir, "test.txt")
62+
testContent := "This is a test file."
63+
err = os.WriteFile(testFile, []byte(testContent), 0o600)
64+
require.NoError(t, err)
65+
66+
// Set up Gin router with gzip middleware and static file serving
67+
gin.SetMode(gin.TestMode)
68+
router := gin.New()
69+
router.Use(Gzip(DefaultCompression))
70+
router.Static("/static", tmpDir)
71+
72+
// Test static file request without gzip support
73+
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/static/test.txt", nil)
74+
// No Accept-Encoding header
75+
76+
w := httptest.NewRecorder()
77+
router.ServeHTTP(w, req)
78+
79+
// The response should be successful and not compressed
80+
assert.Equal(t, http.StatusOK, w.Code)
81+
assert.Equal(t, "", w.Header().Get(headerContentEncoding), "Content should not be compressed")
82+
assert.Equal(t, "", w.Header().Get(headerVary), "Vary header should not be set")
83+
assert.Equal(t, testContent, w.Body.String(), "Content should match original")
84+
}
85+
86+
func TestStaticFileNotFound(t *testing.T) {
87+
// Create a temporary directory (but no files)
88+
tmpDir, err := os.MkdirTemp("", "gzip_static_test")
89+
require.NoError(t, err)
90+
defer os.RemoveAll(tmpDir)
91+
92+
// Set up Gin router with gzip middleware and static file serving
93+
gin.SetMode(gin.TestMode)
94+
router := gin.New()
95+
router.Use(Gzip(DefaultCompression))
96+
router.Static("/static", tmpDir)
97+
98+
// Test request for non-existent file
99+
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/static/nonexistent.txt", nil)
100+
req.Header.Add(headerAcceptEncoding, "gzip")
101+
102+
w := httptest.NewRecorder()
103+
router.ServeHTTP(w, req)
104+
105+
// The response should be 404 and not compressed (this should work correctly)
106+
assert.Equal(t, http.StatusNotFound, w.Code)
107+
assert.Equal(t, "", w.Header().Get(headerContentEncoding), "404 response should not be compressed")
108+
assert.Equal(t, "", w.Header().Get(headerVary), "Vary header should be removed for error responses")
109+
}
110+
111+
func TestStaticDirectoryListing(t *testing.T) {
112+
// Create a temporary directory with a file
113+
tmpDir, err := os.MkdirTemp("", "gzip_static_test")
114+
require.NoError(t, err)
115+
defer os.RemoveAll(tmpDir)
116+
117+
// Create a test file
118+
testFile := filepath.Join(tmpDir, "test.txt")
119+
err = os.WriteFile(testFile, []byte("test content"), 0o600)
120+
require.NoError(t, err)
121+
122+
// Set up Gin router with gzip middleware and static file serving
123+
gin.SetMode(gin.TestMode)
124+
router := gin.New()
125+
router.Use(Gzip(DefaultCompression))
126+
router.Static("/static", tmpDir)
127+
128+
// Test directory listing request
129+
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/static/", nil)
130+
req.Header.Add(headerAcceptEncoding, "gzip")
131+
132+
w := httptest.NewRecorder()
133+
router.ServeHTTP(w, req)
134+
135+
// Note: Gin's default static handler doesn't enable directory listing
136+
// so this will return 404, which should NOT be compressed
137+
assert.Equal(t, http.StatusNotFound, w.Code)
138+
assert.Equal(t, "", w.Header().Get(headerContentEncoding), "404 response should not be compressed")
139+
assert.Equal(t, "", w.Header().Get(headerVary), "Vary header should be removed for error responses")
140+
}
141+
142+
// This test demonstrates the specific issue mentioned in #122
143+
func TestStaticFileGzipHeadersBug(t *testing.T) {
144+
// Create a temporary directory and file for testing
145+
tmpDir, err := os.MkdirTemp("", "gzip_static_test")
146+
require.NoError(t, err)
147+
defer os.RemoveAll(tmpDir)
148+
149+
// Create a test file
150+
testFile := filepath.Join(tmpDir, "test.js")
151+
testContent := "console.log('This is a JavaScript file that should be compressed when served as a static file');"
152+
err = os.WriteFile(testFile, []byte(testContent), 0o600)
153+
require.NoError(t, err)
154+
155+
// Set up Gin router with gzip middleware and static file serving
156+
gin.SetMode(gin.TestMode)
157+
router := gin.New()
158+
router.Use(Gzip(DefaultCompression))
159+
router.Static("/assets", tmpDir)
160+
161+
// Test static file request with gzip support
162+
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/assets/test.js", nil)
163+
req.Header.Add(headerAcceptEncoding, "gzip")
164+
165+
w := httptest.NewRecorder()
166+
router.ServeHTTP(w, req)
167+
168+
t.Logf("Response Status: %d", w.Code)
169+
t.Logf("Content-Encoding: %s", w.Header().Get(headerContentEncoding))
170+
t.Logf("Vary: %s", w.Header().Get(headerVary))
171+
t.Logf("Content-Length: %s", w.Header().Get("Content-Length"))
172+
t.Logf("Body Length: %d", w.Body.Len())
173+
174+
// This test will currently fail due to the bug described in issue #122
175+
// The static handler sets status to 404 initially, causing gzip middleware to remove headers
176+
assert.Equal(t, http.StatusOK, w.Code)
177+
178+
// These assertions will fail with the current bug:
179+
// - Content-Encoding header will be empty instead of "gzip"
180+
// - Vary header will be empty instead of "Accept-Encoding"
181+
// - Content will not be compressed
182+
if w.Header().Get(headerContentEncoding) != "gzip" {
183+
t.Errorf("BUG REPRODUCED: Static file is not being gzip compressed. Content-Encoding: %q, expected: %q",
184+
w.Header().Get(headerContentEncoding), "gzip")
185+
}
186+
187+
if w.Header().Get(headerVary) != headerAcceptEncoding {
188+
t.Errorf("BUG REPRODUCED: Vary header missing. Vary: %q, expected: %q",
189+
w.Header().Get(headerVary), headerAcceptEncoding)
190+
}
191+
}

0 commit comments

Comments
 (0)