From f967a163dbb89e54a46b360f6f695948c13178a5 Mon Sep 17 00:00:00 2001 From: nk-batman21 Date: Fri, 23 Oct 2026 02:36:21 +0700 Subject: [PATCH] fix(middleware): return 400 for invalid gzip request bodies in Decompress When a request has Content-Encoding: gzip but the body does not start with a valid gzip header (a plain body, or a truncated one), gzip.Reader.Reset fails with gzip.ErrHeader or io.ErrUnexpectedEOF. Decompress returned that error as is, so the default error handler answered 500 for malformed client input. Corrupt data after a valid header already ends up as 400 through Bind. Wrap the error as 400 Bad Request, keeping errors that already carry a status code (such as a BodyLimit 413), the same way the JSON serializer handles body read errors. --- middleware/decompress.go | 5 +++- middleware/decompress_test.go | 54 +++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/middleware/decompress.go b/middleware/decompress.go index 355455ce6..b7475975f 100644 --- a/middleware/decompress.go +++ b/middleware/decompress.go @@ -104,7 +104,10 @@ func (config DecompressConfig) ToMiddleware() (echo.MiddlewareFunc, error) { if err == io.EOF { //ignore if body is empty return next(c) } - return err + if echo.StatusCode(err) != 0 { + return err + } + return echo.ErrBadRequest.Wrap(err) } // only Close gzip reader if it was set to a proper gzip source otherwise it will panic on close. diff --git a/middleware/decompress_test.go b/middleware/decompress_test.go index b154dd5b2..33e568499 100644 --- a/middleware/decompress_test.go +++ b/middleware/decompress_test.go @@ -158,6 +158,60 @@ func TestDecompressErrorReturned(t *testing.T) { assert.Empty(t, rec.Header().Get(echo.HeaderContentEncoding)) } +func TestDecompressInvalidGzipBody(t *testing.T) { + gz, err := gzipString(`{"name":"echo"}`) + assert.NoError(t, err) + + var testCases = []struct { + name string + whenBody []byte + }{ + { + name: "body is not gzip", + whenBody: []byte(`{"name":"echo"}`), + }, + { + name: "gzip header is truncated", + whenBody: gz[:4], + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + e := echo.New() + e.Use(Decompress()) + e.POST("/", func(c *echo.Context) error { + return c.NoContent(http.StatusOK) + }) + req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(tc.whenBody)) + req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding) + rec := httptest.NewRecorder() + + e.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusBadRequest, rec.Code) + }) + } +} + +func TestDecompressKeepsBodyLimitError(t *testing.T) { + gz, err := gzipString(`{"name":"echo"}`) + assert.NoError(t, err) + + e := echo.New() + e.Use(BodyLimit(4), Decompress()) + e.POST("/", func(c *echo.Context) error { + return c.NoContent(http.StatusOK) + }) + req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(gz)) + req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding) + req.ContentLength = -1 + rec := httptest.NewRecorder() + + e.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusRequestEntityTooLarge, rec.Code) +} + func TestDecompressSkipper(t *testing.T) { e := echo.New() e.Use(DecompressWithConfig(DecompressConfig{