-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1687 from arun0009/master
adding decompress gzipped request middleware
- Loading branch information
Showing
3 changed files
with
213 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package middleware | ||
|
||
import ( | ||
"bytes" | ||
"compress/gzip" | ||
"github.com/labstack/echo/v4" | ||
"io" | ||
"io/ioutil" | ||
) | ||
|
||
type ( | ||
// DecompressConfig defines the config for Decompress middleware. | ||
DecompressConfig struct { | ||
// Skipper defines a function to skip middleware. | ||
Skipper Skipper | ||
} | ||
) | ||
|
||
//GZIPEncoding content-encoding header if set to "gzip", decompress body contents. | ||
const GZIPEncoding string = "gzip" | ||
|
||
var ( | ||
//DefaultDecompressConfig defines the config for decompress middleware | ||
DefaultDecompressConfig = DecompressConfig{Skipper: DefaultSkipper} | ||
) | ||
|
||
//Decompress decompresses request body based if content encoding type is set to "gzip" with default config | ||
func Decompress() echo.MiddlewareFunc { | ||
return DecompressWithConfig(DefaultDecompressConfig) | ||
} | ||
|
||
//DecompressWithConfig decompresses request body based if content encoding type is set to "gzip" with config | ||
func DecompressWithConfig(config DecompressConfig) echo.MiddlewareFunc { | ||
return func(next echo.HandlerFunc) echo.HandlerFunc { | ||
return func(c echo.Context) error { | ||
if config.Skipper(c) { | ||
return next(c) | ||
} | ||
switch c.Request().Header.Get(echo.HeaderContentEncoding) { | ||
case GZIPEncoding: | ||
gr, err := gzip.NewReader(c.Request().Body) | ||
if err != nil { | ||
if err == io.EOF { //ignore if body is empty | ||
return next(c) | ||
} | ||
return err | ||
} | ||
defer gr.Close() | ||
var buf bytes.Buffer | ||
io.Copy(&buf, gr) | ||
r := ioutil.NopCloser(&buf) | ||
defer r.Close() | ||
c.Request().Body = r | ||
} | ||
return next(c) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
package middleware | ||
|
||
import ( | ||
"bytes" | ||
"compress/gzip" | ||
"io/ioutil" | ||
"net/http" | ||
"net/http/httptest" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/labstack/echo/v4" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestDecompress(t *testing.T) { | ||
e := echo.New() | ||
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("test")) | ||
rec := httptest.NewRecorder() | ||
c := e.NewContext(req, rec) | ||
|
||
// Skip if no Content-Encoding header | ||
h := Decompress()(func(c echo.Context) error { | ||
c.Response().Write([]byte("test")) // For Content-Type sniffing | ||
return nil | ||
}) | ||
h(c) | ||
|
||
assert := assert.New(t) | ||
assert.Equal("test", rec.Body.String()) | ||
|
||
// Decompress | ||
body := `{"name": "echo"}` | ||
gz, _ := gzipString(body) | ||
req = httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(gz))) | ||
req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding) | ||
rec = httptest.NewRecorder() | ||
c = e.NewContext(req, rec) | ||
h(c) | ||
assert.Equal(GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding)) | ||
b, err := ioutil.ReadAll(req.Body) | ||
assert.NoError(err) | ||
assert.Equal(body, string(b)) | ||
} | ||
|
||
func TestCompressRequestWithoutDecompressMiddleware(t *testing.T) { | ||
e := echo.New() | ||
body := `{"name":"echo"}` | ||
gz, _ := gzipString(body) | ||
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(gz))) | ||
req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding) | ||
rec := httptest.NewRecorder() | ||
e.NewContext(req, rec) | ||
e.ServeHTTP(rec, req) | ||
assert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding)) | ||
b, err := ioutil.ReadAll(req.Body) | ||
assert.NoError(t, err) | ||
assert.NotEqual(t, b, body) | ||
assert.Equal(t, b, gz) | ||
} | ||
|
||
func TestDecompressNoContent(t *testing.T) { | ||
e := echo.New() | ||
req := httptest.NewRequest(http.MethodGet, "/", nil) | ||
req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding) | ||
rec := httptest.NewRecorder() | ||
c := e.NewContext(req, rec) | ||
h := Decompress()(func(c echo.Context) error { | ||
return c.NoContent(http.StatusNoContent) | ||
}) | ||
if assert.NoError(t, h(c)) { | ||
assert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding)) | ||
assert.Empty(t, rec.Header().Get(echo.HeaderContentType)) | ||
assert.Equal(t, 0, len(rec.Body.Bytes())) | ||
} | ||
} | ||
|
||
func TestDecompressErrorReturned(t *testing.T) { | ||
e := echo.New() | ||
e.Use(Decompress()) | ||
e.GET("/", func(c echo.Context) error { | ||
return echo.ErrNotFound | ||
}) | ||
req := httptest.NewRequest(http.MethodGet, "/", nil) | ||
req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding) | ||
rec := httptest.NewRecorder() | ||
e.ServeHTTP(rec, req) | ||
assert.Equal(t, http.StatusNotFound, rec.Code) | ||
assert.Empty(t, rec.Header().Get(echo.HeaderContentEncoding)) | ||
} | ||
|
||
func TestDecompressSkipper(t *testing.T) { | ||
e := echo.New() | ||
e.Use(DecompressWithConfig(DecompressConfig{ | ||
Skipper: func(c echo.Context) bool { | ||
return c.Request().URL.Path == "/skip" | ||
}, | ||
})) | ||
body := `{"name": "echo"}` | ||
req := httptest.NewRequest(http.MethodPost, "/skip", strings.NewReader(body)) | ||
req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding) | ||
rec := httptest.NewRecorder() | ||
c := e.NewContext(req, rec) | ||
e.ServeHTTP(rec, req) | ||
assert.Equal(t, rec.Header().Get(echo.HeaderContentType), echo.MIMEApplicationJSONCharsetUTF8) | ||
reqBody, err := ioutil.ReadAll(c.Request().Body) | ||
assert.NoError(t, err) | ||
assert.Equal(t, body, string(reqBody)) | ||
} | ||
|
||
func BenchmarkDecompress(b *testing.B) { | ||
e := echo.New() | ||
body := `{"name": "echo"}` | ||
gz, _ := gzipString(body) | ||
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(gz))) | ||
req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding) | ||
|
||
h := Decompress()(func(c echo.Context) error { | ||
c.Response().Write([]byte(body)) // For Content-Type sniffing | ||
return nil | ||
}) | ||
|
||
b.ReportAllocs() | ||
b.ResetTimer() | ||
|
||
for i := 0; i < b.N; i++ { | ||
// Decompress | ||
rec := httptest.NewRecorder() | ||
c := e.NewContext(req, rec) | ||
h(c) | ||
} | ||
} | ||
|
||
func gzipString(body string) ([]byte, error) { | ||
var buf bytes.Buffer | ||
gz := gzip.NewWriter(&buf) | ||
|
||
_, err := gz.Write([]byte(body)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if err := gz.Close(); err != nil { | ||
return nil, err | ||
} | ||
|
||
return buf.Bytes(), nil | ||
} |