Repository: labstack/echo
Pull request #2481 (2023-07-16 17:22:55)

fix gzip not sending response code for no content responses (404, 301/302 redirects etc)
This is fix for #2480

Response code is not written from buffered writer to the actual writer when  handler sent no-content response - ala 404 or redirect

```go
func main() {
	e := echo.New()

	e.Use(middleware.Gzip())

	e.GET("/404", func(ctx echo.Context) error {
		return ctx.NoContent(http.StatusNotFound)
	})

	e.GET("/redirect", func(ctx echo.Context) error {
		return ctx.Redirect(http.StatusTemporaryRedirect, "/login")
	})

	if err := e.Start(":8080"); err != nil && !errors.Is(err, http.ErrServerClosed) {
		log.Fatal(err)
	}
}
```

Example output after fix:
```bash
x@x:~/$ curl -v --compressed "http://localhost:8080/404"
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /404 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.88.1
> Accept: */*
> Accept-Encoding: deflate, gzip, br, zstd
> 
< HTTP/1.1 404 Not Found
< Vary: Accept-Encoding
< Date: Sun, 16 Jul 2023 17:20:11 GMT
< Content-Length: 0
< 
* Connection #0 to host localhost left intact

x@x:~/$ curl -v --compressed "http://localhost:8080/redirect"
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /redirect HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.88.1
> Accept: */*
> Accept-Encoding: deflate, gzip, br, zstd
> 
< HTTP/1.1 307 Temporary Redirect
< Location: /login
< Vary: Accept-Encoding
< Date: Sun, 16 Jul 2023 17:20:16 GMT
< Content-Length: 0
< 
* Connection #0 to host localhost left intact
```

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/labstack/echo/pull/2481?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) Report
Patch coverage: **`100.00`**% and no project coverage change.
> Comparison is base [(`60af056`)](https://app.codecov.io/gh/labstack/echo/commit/60af056959d5ddfb0e8db8dec2f597d72e27a58b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) 92.81% compared to head [(`65ee717`)](https://app.codecov.io/gh/labstack/echo/pull/2481?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) 92.82%.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #2481   +/-   ##
=======================================
  Coverage   92.81%   92.82%           
=======================================
  Files          39       39           
  Lines        4618     4624    +6     
=======================================
+ Hits         4286     4292    +6     
  Misses        241      241           
  Partials       91       91           
```


| [Impacted Files](https://app.codecov.io/gh/labstack/echo/pull/2481?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) | Coverage Δ | |
|---|---|---|
| [middleware/compress.go](https://app.codecov.io/gh/labstack/echo/pull/2481?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack#diff-bWlkZGxld2FyZS9jb21wcmVzcy5nbw==) | `86.99% <100.00%> (+0.66%)` | :arrow_up: |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/labstack/echo/pull/2481?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack).


Patch:
diff --git a/middleware/compress.go b/middleware/compress.go
index cbe29fc..3e9bd32 100644
--- a/middleware/compress.go
+++ b/middleware/compress.go
@@ -107,10 +107,16 @@ func GzipWithConfig(config GzipConfig) echo.MiddlewareFunc {
 
 				grw := &gzipResponseWriter{Writer: w, ResponseWriter: rw, minLength: config.MinLength, buffer: buf}
 				defer func() {
+					// There are different reasons for cases when we have not yet written response to the client and now need to do so.
+					// a) handler response had only response code and no response body (ala 404 or redirects etc). Response code need to be written now.
+					// b) body is shorter than our minimum length threshold and being buffered currently and needs to be written
 					if !grw.wroteBody {
 						if res.Header().Get(echo.HeaderContentEncoding) == gzipScheme {
 							res.Header().Del(echo.HeaderContentEncoding)
 						}
+						if grw.wroteHeader {
+							rw.WriteHeader(grw.code)
+						}
 						// We have to reset response to it's pristine state when
 						// nothing is written to body or error is returned.
 						// See issue #424, #407.


Test patch:
diff --git a/middleware/compress_test.go b/middleware/compress_test.go
index e43e2d6..0ed16c8 100644
--- a/middleware/compress_test.go
+++ b/middleware/compress_test.go
@@ -89,8 +89,6 @@ func TestGzip(t *testing.T) {
 }
 
 func TestGzipWithMinLength(t *testing.T) {
-	assert := assert.New(t)
-
 	e := echo.New()
 	// Minimal response length
 	e.Use(GzipWithConfig(GzipConfig{MinLength: 10}))
@@ -103,19 +101,17 @@ func TestGzipWithMinLength(t *testing.T) {
 	req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)
 	rec := httptest.NewRecorder()
 	e.ServeHTTP(rec, req)
-	assert.Equal(gzipScheme, rec.Header().Get(echo.HeaderContentEncoding))
+	assert.Equal(t, gzipScheme, rec.Header().Get(echo.HeaderContentEncoding))
 	r, err := gzip.NewReader(rec.Body)
-	if assert.NoError(err) {
+	if assert.NoError(t, err) {
 		buf := new(bytes.Buffer)
 		defer r.Close()
 		buf.ReadFrom(r)
-		assert.Equal("foobarfoobar", buf.String())
+		assert.Equal(t, "foobarfoobar", buf.String())
 	}
 }
 
 func TestGzipWithMinLengthTooShort(t *testing.T) {
-	assert := assert.New(t)
-
 	e := echo.New()
 	// Minimal response length
 	e.Use(GzipWithConfig(GzipConfig{MinLength: 10}))
@@ -127,13 +123,29 @@ func TestGzipWithMinLengthTooShort(t *testing.T) {
 	req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)
 	rec := httptest.NewRecorder()
 	e.ServeHTTP(rec, req)
-	assert.Equal("", rec.Header().Get(echo.HeaderContentEncoding))
-	assert.Contains(rec.Body.String(), "test")
+	assert.Equal(t, "", rec.Header().Get(echo.HeaderContentEncoding))
+	assert.Contains(t, rec.Body.String(), "test")
 }
 
-func TestGzipWithMinLengthChunked(t *testing.T) {
-	assert := assert.New(t)
+func TestGzipWithResponseWithoutBody(t *testing.T) {
+	e := echo.New()
+
+	e.Use(Gzip())
+	e.GET("/", func(c echo.Context) error {
+		return c.Redirect(http.StatusMovedPermanently, "http://localhost")
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)
+	rec := httptest.NewRecorder()
 
+	e.ServeHTTP(rec, req)
+
+	assert.Equal(t, http.StatusMovedPermanently, rec.Code)
+	assert.Equal(t, "", rec.Header().Get(echo.HeaderContentEncoding))
+}
+
+func TestGzipWithMinLengthChunked(t *testing.T) {
 	e := echo.New()
 
 	// Gzip chunked
@@ -155,36 +167,36 @@ func TestGzipWithMinLengthChunked(t *testing.T) {
 		c.Response().Flush()
 
 		// Read the first part of the data
-		assert.True(rec.Flushed)
-		assert.Equal(gzipScheme, rec.Header().Get(echo.HeaderContentEncoding))
+		assert.True(t, rec.Flushed)
+		assert.Equal(t, gzipScheme, rec.Header().Get(echo.HeaderContentEncoding))
 
 		var err error
 		r, err = gzip.NewReader(rec.Body)
-		assert.NoError(err)
+		assert.NoError(t, err)
 
 		_, err = io.ReadFull(r, chunkBuf)
-		assert.NoError(err)
-		assert.Equal("test\n", string(chunkBuf))
+		assert.NoError(t, err)
+		assert.Equal(t, "test\n", string(chunkBuf))
 
 		// Write and flush the second part of the data
 		c.Response().Write([]byte("test\n"))
 		c.Response().Flush()
 
 		_, err = io.ReadFull(r, chunkBuf)
-		assert.NoError(err)
-		assert.Equal("test\n", string(chunkBuf))
+		assert.NoError(t, err)
+		assert.Equal(t, "test\n", string(chunkBuf))
 
 		// Write the final part of the data and return
 		c.Response().Write([]byte("test"))
 		return nil
 	})(c)
 
-	assert.NotNil(r)
+	assert.NotNil(t, r)
 
 	buf := new(bytes.Buffer)
 
 	buf.ReadFrom(r)
-	assert.Equal("test", buf.String())
+	assert.Equal(t, "test", buf.String())
 
 	r.Close()
 }