Skip to content

Commit 7770763

Browse files
committed
Feat:
- Let "ReadBody" return more readable error - Optimize the performance of "CatURL" func
1 parent abf20c8 commit 7770763

5 files changed

Lines changed: 381 additions & 18 deletions

File tree

network/http/gin.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@
66
package http
77

88
import (
9+
"bufio"
910
"bytes"
1011
"compress/gzip"
1112
"crypto/md5" //nolint:gosec
13+
"encoding/hex"
1214
"fmt"
15+
"hash"
16+
"io"
1317
"io/ioutil"
1418
"net"
1519
"net/http"
@@ -268,6 +272,7 @@ func RequestLoggerMiddleware(c *gin.Context) {
268272
body[:len(body)%MaxRequestBodyLen]+"...")
269273
}
270274

275+
// Deprecated, use GzipReadWithMD5 instead
271276
func GinReadWithMD5(c *gin.Context) (buf []byte, md5str string, err error) {
272277
buf, err = readBody(c)
273278
if err != nil {
@@ -283,6 +288,7 @@ func GinReadWithMD5(c *gin.Context) (buf []byte, md5str string, err error) {
283288
return
284289
}
285290

291+
// Deprecated, use GzipRead instead.
286292
func GinRead(c *gin.Context) (buf []byte, err error) {
287293
buf, err = readBody(c)
288294
if err != nil {
@@ -307,6 +313,106 @@ func GinGetArg(c *gin.Context, hdr, param string) (v string, err error) {
307313
return
308314
}
309315

316+
type ReaderWithHash struct {
317+
r io.Reader
318+
h hash.Hash
319+
}
320+
321+
func NewReaderWithHash(r io.Reader, h hash.Hash) *ReaderWithHash {
322+
return &ReaderWithHash{
323+
r: r,
324+
h: h,
325+
}
326+
}
327+
328+
func (h *ReaderWithHash) Read(p []byte) (int, error) {
329+
n, err := h.r.Read(p)
330+
if n > 0 {
331+
_, we := h.h.Write(p[:n])
332+
if we != nil {
333+
return n, fmt.Errorf("unable to write data to hasher: %w", we)
334+
}
335+
}
336+
return n, err
337+
}
338+
339+
func (h *ReaderWithHash) Sum() []byte {
340+
return h.h.Sum(nil)
341+
}
342+
343+
func (h *ReaderWithHash) SumHex() string {
344+
return hex.EncodeToString(h.Sum())
345+
}
346+
347+
func (h *ReaderWithHash) Close() error {
348+
return nil
349+
}
350+
351+
func gzipReadMD5AndClose(req *http.Request, md5Sum bool, closeBody bool) ([]byte, string, error) {
352+
353+
var (
354+
rc io.ReadCloser
355+
rh *ReaderWithHash
356+
)
357+
358+
rc = req.Body
359+
if closeBody {
360+
defer req.Body.Close()
361+
}
362+
363+
if md5Sum {
364+
rh = NewReaderWithHash(rc, md5.New())
365+
defer rh.Close()
366+
rc = rh
367+
}
368+
369+
// as an HTTP server, we do not need to close the Body
370+
switch req.Header.Get("Content-Encoding") {
371+
case "gzip":
372+
bufReader := bufio.NewReader(rc)
373+
magic, err := bufReader.Peek(len(GzipMagic))
374+
if err != nil {
375+
return nil, "", fmt.Errorf("unable to peek 2 bytes from Body: %w", err)
376+
}
377+
378+
if bytes.Compare(GzipMagic, magic) == 0 {
379+
rc, err = gzip.NewReader(bufReader)
380+
if err != nil {
381+
return nil, "", fmt.Errorf("unable to init gzip reader: %w", err)
382+
}
383+
defer rc.Close()
384+
} else {
385+
l.Warnf(`illegal gzip format while Content-Encoding = gzip, magic %v expected, got %v`, GzipMagic, magic)
386+
rc = io.NopCloser(bufReader)
387+
}
388+
}
389+
390+
body, err := io.ReadAll(rc)
391+
if err != nil {
392+
return nil, "", fmt.Errorf("unable to successfully read: %w, bytes has read: %d, http Content-Length: %d", err, len(body), req.ContentLength)
393+
}
394+
395+
if md5Sum && rh != nil {
396+
return body, rh.SumHex(), nil
397+
}
398+
399+
return body, "", nil
400+
401+
}
402+
403+
// GzipReadWithMD5 will automatically unzip the Request.Body and calculate its MD5 sum,
404+
// it WILL close the Request.Body on return.
405+
func GzipReadWithMD5(req *http.Request) ([]byte, string, error) {
406+
return gzipReadMD5AndClose(req, true, true)
407+
}
408+
409+
// GzipRead will automatically unzip the Request.Body, it WILL close the Request.Body on return.
410+
func GzipRead(req *http.Request) ([]byte, error) {
411+
body, _, err := gzipReadMD5AndClose(req, false, true)
412+
return body, err
413+
}
414+
415+
// Deprecated, you should first consider using GzipRead or GzipReadWithMD5
310416
func Unzip(in []byte) (out []byte, err error) {
311417
gzr, err := gzip.NewReader(bytes.NewBuffer(in))
312418
if err != nil {

network/http/gin_test.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,44 @@
66
package http
77

88
import (
9+
"bytes"
10+
"compress/gzip"
11+
"crypto/md5"
12+
"encoding/hex"
13+
"fmt"
914
"github.com/GuanceCloud/cliutils/testutil"
1015
"io"
1116
"net/http"
17+
"strings"
1218
"testing"
1319
"time"
1420

1521
"github.com/gin-gonic/gin"
1622
)
1723

24+
const testText = `观测云提供的系统全链路可观测解决方案,
25+
可实现从底层基础设施到通用技术组件,
26+
再到业务应用系统的全链路可观测,
27+
将不可预知性变为确定已知性。
28+
观测云提供快速实现系统可观测的解决方案,满足云、云原生、应用和业务上的监测需求。
29+
通过自定义监测方案,实现实时可交互仪表板、高效观测基础设施、全链路应用性能可观测等功能,保障系统稳定性
30+
观测云、全链路可观测、实时监测、自定义监测、云原生
31+
观测云提供的系统全链路可观测解决方案,
32+
可实现从底层基础设施到通用技术组件,
33+
再到业务应用系统的全链路可观测,
34+
将不可预知性变为确定已知性。
35+
观测云提供快速实现系统可观测的解决方案,满足云、云原生、应用和业务上的监测需求。
36+
通过自定义监测方案,实现实时可交互仪表板、高效观测基础设施、全链路应用性能可观测等功能,保障系统稳定性
37+
观测云、全链路可观测、实时监测、自定义监测、云原生
38+
观测云提供的系统全链路可观测解决方案,
39+
可实现从底层基础设施到通用技术组件,
40+
再到业务应用系统的全链路可观测,
41+
将不可预知性变为确定已知性。
42+
观测云提供快速实现系统可观测的解决方案,满足云、云原生、应用和业务上的监测需求。
43+
通过自定义监测方案,实现实时可交互仪表板、高效观测基础设施、全链路应用性能可观测等功能,保障系统稳定性
44+
观测云、全链路可观测、实时监测、自定义监测、云原生
45+
`
46+
1847
func BenchmarkAllMiddlewares(b *testing.B) {
1948
cases := []struct {
2049
name string
@@ -200,3 +229,137 @@ func TestMiddlewares(t *testing.T) {
200229
resp.Body.Close()
201230
}
202231
}
232+
233+
func TestNewHashReader(t *testing.T) {
234+
src := []byte(testText)
235+
236+
r := NewReaderWithHash(bytes.NewReader(src), md5.New())
237+
238+
all, err := io.ReadAll(r)
239+
testutil.Ok(t, err)
240+
testutil.Equals(t, src, all)
241+
242+
md5Sum := md5.Sum(src)
243+
s := r.Sum()
244+
245+
testutil.Equals(t, md5Sum[:], s)
246+
247+
fmt.Println(r.SumHex())
248+
fmt.Println(hex.EncodeToString(md5Sum[:]))
249+
testutil.Equals(t, hex.EncodeToString(md5Sum[:]), r.SumHex())
250+
}
251+
252+
type testCase struct {
253+
name string
254+
body []byte
255+
contentEncoding string
256+
}
257+
258+
func TestGzipReadWithMD5(t *testing.T) {
259+
260+
gzipOut := &bytes.Buffer{}
261+
gw := gzip.NewWriter(gzipOut)
262+
_, err := gw.Write([]byte(testText))
263+
testutil.Ok(t, err)
264+
err = gw.Close()
265+
testutil.Ok(t, err)
266+
267+
testCases := []testCase{
268+
{
269+
name: "plain body",
270+
body: []byte(testText),
271+
contentEncoding: "",
272+
},
273+
{
274+
name: "gzip body",
275+
body: gzipOut.Bytes(),
276+
contentEncoding: "gzip",
277+
},
278+
}
279+
280+
for _, tc := range testCases {
281+
t.Run(tc.name, func(t *testing.T) {
282+
283+
req1, err := http.NewRequest(http.MethodPost, "/", bytes.NewReader(tc.body))
284+
testutil.Ok(t, err)
285+
if tc.contentEncoding != "" {
286+
req1.Header.Set("Content-Encoding", tc.contentEncoding)
287+
}
288+
289+
req2, err := http.NewRequest(http.MethodPost, "/", bytes.NewReader(tc.body))
290+
testutil.Ok(t, err)
291+
if tc.contentEncoding != "" {
292+
req2.Header.Set("Content-Encoding", tc.contentEncoding)
293+
}
294+
295+
ginCtx := &gin.Context{Request: req1}
296+
body1, sum1, err := GinReadWithMD5(ginCtx)
297+
testutil.Ok(t, err)
298+
299+
body2, sum2, err := GzipReadWithMD5(req2)
300+
testutil.Ok(t, err)
301+
302+
testutil.Equals(t, body1, body2)
303+
testutil.Equals(t, sum1, sum2)
304+
305+
})
306+
}
307+
}
308+
309+
func BenchmarkGzipReadWithMD5(b *testing.B) {
310+
311+
text := strings.Repeat(testText, 100000)
312+
313+
gzipOut := &bytes.Buffer{}
314+
gw := gzip.NewWriter(gzipOut)
315+
_, err := gw.Write([]byte(text))
316+
testutil.Ok(b, err)
317+
err = gw.Close()
318+
testutil.Ok(b, err)
319+
320+
sr := strings.NewReader(text)
321+
322+
b.Run("GinRead", func(t *testing.B) {
323+
sr.Reset(text)
324+
325+
req, err := http.NewRequest(http.MethodPost, "/", sr)
326+
testutil.Ok(t, err)
327+
328+
_, err = GinRead(&gin.Context{Request: req})
329+
testutil.Ok(t, err)
330+
})
331+
332+
b.Run("GzipRead", func(t *testing.B) {
333+
334+
sr.Reset(text)
335+
336+
req, err := http.NewRequest(http.MethodPost, "/", sr)
337+
testutil.Ok(t, err)
338+
339+
_, err = GzipRead(req)
340+
testutil.Ok(t, err)
341+
})
342+
343+
b.Run("GinReadWithMD5", func(t *testing.B) {
344+
345+
req, err := http.NewRequest(http.MethodPost, "/", bytes.NewReader(gzipOut.Bytes()))
346+
testutil.Ok(t, err)
347+
req.Header.Set("Content-Encoding", "gzip")
348+
349+
body, _, err := GinReadWithMD5(&gin.Context{Request: req})
350+
testutil.Ok(t, err)
351+
testutil.Equals(t, string(body), text)
352+
})
353+
354+
b.Run("GzipReadWithMD5", func(t *testing.B) {
355+
356+
req, err := http.NewRequest(http.MethodPost, "/", bytes.NewReader(gzipOut.Bytes()))
357+
testutil.Ok(t, err)
358+
req.Header.Set("Content-Encoding", "gzip")
359+
360+
body, _, err := GzipReadWithMD5(req)
361+
testutil.Ok(t, err)
362+
testutil.Equals(t, string(body), text)
363+
})
364+
365+
}

network/http/http.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,22 @@
77
package http
88

99
import (
10-
"io/ioutil"
1110
"net/http"
1211
)
1312

14-
// ReadBody will automatically unzip body.
15-
func ReadBody(req *http.Request) ([]byte, error) {
16-
buf, err := ioutil.ReadAll(req.Body)
17-
if err != nil {
18-
return nil, err
19-
}
13+
var (
14+
// ZIPMagic see https://en.wikipedia.org/wiki/ZIP_(file_format)#Local_file_header
15+
ZIPMagic = []byte{0x50, 0x4b, 0x3, 0x4} //
16+
17+
// LZ4Magic see https://android.googlesource.com/platform/external/lz4/+/HEAD/doc/lz4_Frame_format.md#general-structure-of-lz4-frame-format
18+
LZ4Magic = []byte{0x4, 0x22, 0x4d, 0x18}
2019

21-
// as HTTP server, we do not need to close body
22-
switch req.Header.Get("Content-Encoding") {
23-
case "gzip":
24-
return Unzip(buf)
25-
default:
26-
return buf, err
27-
}
20+
// GzipMagic see https://en.wikipedia.org/wiki/Gzip#File_format
21+
GzipMagic = []byte{0x1f, 0x8b}
22+
)
23+
24+
// ReadBody will automatically unzip the body, it doesn't close the Request.Body.
25+
func ReadBody(req *http.Request) ([]byte, error) {
26+
body, _, err := gzipReadMD5AndClose(req, false, false)
27+
return body, err
2828
}

0 commit comments

Comments
 (0)