Skip to content

Commit ce95e12

Browse files
authored
Merge pull request #1687 from arun0009/master
adding decompress gzipped request middleware
2 parents 7a90304 + b47042f commit ce95e12

File tree

3 files changed

+213
-6
lines changed

3 files changed

+213
-6
lines changed

.github/workflows/echo.yml

+7-6
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@ on:
1818
- '_fixture/**'
1919
- '.github/**'
2020

21-
env:
22-
GO111MODULE: on
23-
GOPROXY: https://proxy.golang.org
24-
2521
jobs:
2622
test:
2723
strategy:
@@ -38,10 +34,15 @@ jobs:
3834

3935
- name: Set GOPATH and PATH
4036
run: |
41-
echo "::set-env name=GOPATH::$(dirname $GITHUB_WORKSPACE)"
42-
echo "::add-path::$(dirname $GITHUB_WORKSPACE)/bin"
37+
echo "GOPATH=$(dirname $GITHUB_WORKSPACE)" >> $GITHUB_ENV
38+
echo "$(dirname $GITHUB_WORKSPACE)/bin" >> $GITHUB_PATH
4339
shell: bash
4440

41+
- name: Set build variables
42+
run: |
43+
echo "GOPROXY=https://proxy.golang.org" >> $GITHUB_ENV
44+
echo "GO111MODULE=on" >> $GITHUB_ENV
45+
4546
- name: Checkout Code
4647
uses: actions/checkout@v1
4748
with:

middleware/decompress.go

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package middleware
2+
3+
import (
4+
"bytes"
5+
"compress/gzip"
6+
"github.com/labstack/echo/v4"
7+
"io"
8+
"io/ioutil"
9+
)
10+
11+
type (
12+
// DecompressConfig defines the config for Decompress middleware.
13+
DecompressConfig struct {
14+
// Skipper defines a function to skip middleware.
15+
Skipper Skipper
16+
}
17+
)
18+
19+
//GZIPEncoding content-encoding header if set to "gzip", decompress body contents.
20+
const GZIPEncoding string = "gzip"
21+
22+
var (
23+
//DefaultDecompressConfig defines the config for decompress middleware
24+
DefaultDecompressConfig = DecompressConfig{Skipper: DefaultSkipper}
25+
)
26+
27+
//Decompress decompresses request body based if content encoding type is set to "gzip" with default config
28+
func Decompress() echo.MiddlewareFunc {
29+
return DecompressWithConfig(DefaultDecompressConfig)
30+
}
31+
32+
//DecompressWithConfig decompresses request body based if content encoding type is set to "gzip" with config
33+
func DecompressWithConfig(config DecompressConfig) echo.MiddlewareFunc {
34+
return func(next echo.HandlerFunc) echo.HandlerFunc {
35+
return func(c echo.Context) error {
36+
if config.Skipper(c) {
37+
return next(c)
38+
}
39+
switch c.Request().Header.Get(echo.HeaderContentEncoding) {
40+
case GZIPEncoding:
41+
gr, err := gzip.NewReader(c.Request().Body)
42+
if err != nil {
43+
if err == io.EOF { //ignore if body is empty
44+
return next(c)
45+
}
46+
return err
47+
}
48+
defer gr.Close()
49+
var buf bytes.Buffer
50+
io.Copy(&buf, gr)
51+
r := ioutil.NopCloser(&buf)
52+
defer r.Close()
53+
c.Request().Body = r
54+
}
55+
return next(c)
56+
}
57+
}
58+
}

middleware/decompress_test.go

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package middleware
2+
3+
import (
4+
"bytes"
5+
"compress/gzip"
6+
"io/ioutil"
7+
"net/http"
8+
"net/http/httptest"
9+
"strings"
10+
"testing"
11+
12+
"github.com/labstack/echo/v4"
13+
"github.com/stretchr/testify/assert"
14+
)
15+
16+
func TestDecompress(t *testing.T) {
17+
e := echo.New()
18+
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("test"))
19+
rec := httptest.NewRecorder()
20+
c := e.NewContext(req, rec)
21+
22+
// Skip if no Content-Encoding header
23+
h := Decompress()(func(c echo.Context) error {
24+
c.Response().Write([]byte("test")) // For Content-Type sniffing
25+
return nil
26+
})
27+
h(c)
28+
29+
assert := assert.New(t)
30+
assert.Equal("test", rec.Body.String())
31+
32+
// Decompress
33+
body := `{"name": "echo"}`
34+
gz, _ := gzipString(body)
35+
req = httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(gz)))
36+
req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)
37+
rec = httptest.NewRecorder()
38+
c = e.NewContext(req, rec)
39+
h(c)
40+
assert.Equal(GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))
41+
b, err := ioutil.ReadAll(req.Body)
42+
assert.NoError(err)
43+
assert.Equal(body, string(b))
44+
}
45+
46+
func TestCompressRequestWithoutDecompressMiddleware(t *testing.T) {
47+
e := echo.New()
48+
body := `{"name":"echo"}`
49+
gz, _ := gzipString(body)
50+
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(gz)))
51+
req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)
52+
rec := httptest.NewRecorder()
53+
e.NewContext(req, rec)
54+
e.ServeHTTP(rec, req)
55+
assert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))
56+
b, err := ioutil.ReadAll(req.Body)
57+
assert.NoError(t, err)
58+
assert.NotEqual(t, b, body)
59+
assert.Equal(t, b, gz)
60+
}
61+
62+
func TestDecompressNoContent(t *testing.T) {
63+
e := echo.New()
64+
req := httptest.NewRequest(http.MethodGet, "/", nil)
65+
req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)
66+
rec := httptest.NewRecorder()
67+
c := e.NewContext(req, rec)
68+
h := Decompress()(func(c echo.Context) error {
69+
return c.NoContent(http.StatusNoContent)
70+
})
71+
if assert.NoError(t, h(c)) {
72+
assert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))
73+
assert.Empty(t, rec.Header().Get(echo.HeaderContentType))
74+
assert.Equal(t, 0, len(rec.Body.Bytes()))
75+
}
76+
}
77+
78+
func TestDecompressErrorReturned(t *testing.T) {
79+
e := echo.New()
80+
e.Use(Decompress())
81+
e.GET("/", func(c echo.Context) error {
82+
return echo.ErrNotFound
83+
})
84+
req := httptest.NewRequest(http.MethodGet, "/", nil)
85+
req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)
86+
rec := httptest.NewRecorder()
87+
e.ServeHTTP(rec, req)
88+
assert.Equal(t, http.StatusNotFound, rec.Code)
89+
assert.Empty(t, rec.Header().Get(echo.HeaderContentEncoding))
90+
}
91+
92+
func TestDecompressSkipper(t *testing.T) {
93+
e := echo.New()
94+
e.Use(DecompressWithConfig(DecompressConfig{
95+
Skipper: func(c echo.Context) bool {
96+
return c.Request().URL.Path == "/skip"
97+
},
98+
}))
99+
body := `{"name": "echo"}`
100+
req := httptest.NewRequest(http.MethodPost, "/skip", strings.NewReader(body))
101+
req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)
102+
rec := httptest.NewRecorder()
103+
c := e.NewContext(req, rec)
104+
e.ServeHTTP(rec, req)
105+
assert.Equal(t, rec.Header().Get(echo.HeaderContentType), echo.MIMEApplicationJSONCharsetUTF8)
106+
reqBody, err := ioutil.ReadAll(c.Request().Body)
107+
assert.NoError(t, err)
108+
assert.Equal(t, body, string(reqBody))
109+
}
110+
111+
func BenchmarkDecompress(b *testing.B) {
112+
e := echo.New()
113+
body := `{"name": "echo"}`
114+
gz, _ := gzipString(body)
115+
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(gz)))
116+
req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)
117+
118+
h := Decompress()(func(c echo.Context) error {
119+
c.Response().Write([]byte(body)) // For Content-Type sniffing
120+
return nil
121+
})
122+
123+
b.ReportAllocs()
124+
b.ResetTimer()
125+
126+
for i := 0; i < b.N; i++ {
127+
// Decompress
128+
rec := httptest.NewRecorder()
129+
c := e.NewContext(req, rec)
130+
h(c)
131+
}
132+
}
133+
134+
func gzipString(body string) ([]byte, error) {
135+
var buf bytes.Buffer
136+
gz := gzip.NewWriter(&buf)
137+
138+
_, err := gz.Write([]byte(body))
139+
if err != nil {
140+
return nil, err
141+
}
142+
143+
if err := gz.Close(); err != nil {
144+
return nil, err
145+
}
146+
147+
return buf.Bytes(), nil
148+
}

0 commit comments

Comments
 (0)