diff --git a/internal/utils/utils_io.go b/internal/utils/utils_io.go new file mode 100644 index 000000000..45dc34470 --- /dev/null +++ b/internal/utils/utils_io.go @@ -0,0 +1,63 @@ +// Copyright 2020 gf Author(https://github.com/gogf/gf). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package utils + +import ( + "io" + "io/ioutil" +) + +// ReadCloser implements the io.ReadCloser interface +// which is used for reading request body content multiple times. +// +// Note that it cannot be closed. +type ReadCloser struct { + index int // Current read position. + content []byte // Content. + repeatable bool +} + +// NewRepeatReadCloser creates and returns a RepeatReadCloser object. +func NewReadCloser(content []byte, repeatable bool) io.ReadCloser { + return &ReadCloser{ + content: content, + repeatable: repeatable, + } +} + +// NewRepeatReadCloserWithReadCloser creates and returns a RepeatReadCloser object +// with given io.ReadCloser. +func NewReadCloserWithReadCloser(r io.ReadCloser, repeatable bool) (io.ReadCloser, error) { + content, err := ioutil.ReadAll(r) + if err != nil { + return nil, err + } + defer r.Close() + return &ReadCloser{ + content: content, + repeatable: repeatable, + }, nil +} + +// Read implements the io.ReadCloser interface. +func (b *ReadCloser) Read(p []byte) (n int, err error) { + n = copy(p, b.content[b.index:]) + b.index += n + if b.index >= len(b.content) { + // Make it repeatable reading. + if b.repeatable { + b.index = 0 + } + return n, io.EOF + } + return n, nil +} + +// Close implements the io.ReadCloser interface. +func (b *ReadCloser) Close() error { + return nil +} diff --git a/internal/utils/utils_z_test.go b/internal/utils/utils_z_test.go new file mode 100644 index 000000000..be66e0730 --- /dev/null +++ b/internal/utils/utils_z_test.go @@ -0,0 +1,65 @@ +// Copyright 2020 gf Author(https://github.com/gogf/gf). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package utils_test + +import ( + "github.com/gogf/gf/internal/utils" + "github.com/gogf/gf/test/gtest" + "io/ioutil" + "testing" +) + +func Test_ReadCloser(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + n int + b = make([]byte, 3) + body = utils.NewReadCloser([]byte{1, 2, 3, 4}, false) + ) + n, _ = body.Read(b) + t.Assert(b[:n], []byte{1, 2, 3}) + n, _ = body.Read(b) + t.Assert(b[:n], []byte{4}) + + n, _ = body.Read(b) + t.Assert(b[:n], []byte{}) + n, _ = body.Read(b) + t.Assert(b[:n], []byte{}) + }) + gtest.C(t, func(t *gtest.T) { + var ( + r []byte + body = utils.NewReadCloser([]byte{1, 2, 3, 4}, false) + ) + r, _ = ioutil.ReadAll(body) + t.Assert(r, []byte{1, 2, 3, 4}) + r, _ = ioutil.ReadAll(body) + t.Assert(r, []byte{}) + }) + gtest.C(t, func(t *gtest.T) { + var ( + n int + r []byte + b = make([]byte, 3) + body = utils.NewReadCloser([]byte{1, 2, 3, 4}, true) + ) + n, _ = body.Read(b) + t.Assert(b[:n], []byte{1, 2, 3}) + n, _ = body.Read(b) + t.Assert(b[:n], []byte{4}) + + n, _ = body.Read(b) + t.Assert(b[:n], []byte{1, 2, 3}) + n, _ = body.Read(b) + t.Assert(b[:n], []byte{4}) + + r, _ = ioutil.ReadAll(body) + t.Assert(r, []byte{1, 2, 3, 4}) + r, _ = ioutil.ReadAll(body) + t.Assert(r, []byte{1, 2, 3, 4}) + }) +} diff --git a/net/ghttp/ghttp_client_dump.go b/net/ghttp/ghttp_client_dump.go index aac669137..2e57e817b 100644 --- a/net/ghttp/ghttp_client_dump.go +++ b/net/ghttp/ghttp_client_dump.go @@ -7,68 +7,41 @@ package ghttp import ( - "bytes" "fmt" + "github.com/gogf/gf/internal/utils" "io/ioutil" "net/http" "net/http/httputil" - "github.com/gogf/gf/text/gstr" "github.com/gogf/gf/util/gconv" ) // dumpTextFormat is the format of the dumped raw string const dumpTextFormat = `+---------------------------------------------+ -| %s | +| %s | +---------------------------------------------+ %s %s ` -// ifDumpBody determines whether to output body according to content-type. -func ifDumpBody(contentType string) bool { - // the body should not be output when the body is html or stream. - if gstr.Contains(contentType, "application/json") || - gstr.Contains(contentType, "application/xml") || - gstr.Contains(contentType, "multipart/form-data") || - gstr.Contains(contentType, "application/x-www-form-urlencoded") || - gstr.Contains(contentType, "text/plain") { - return true - } - return false -} - // getRequestBody returns the raw text of the request body. func getRequestBody(req *http.Request) string { - contentType := req.Header.Get("Content-Type") - if !ifDumpBody(contentType) { + if req.Body == nil { return "" } - // so that the request body can be read again. - bodyReader, errGetBody := req.GetBody() - if errGetBody != nil { - return "" - } - bytesBody, errReadBody := ioutil.ReadAll(bodyReader) - if errReadBody != nil { - return "" - } - return gconv.UnsafeBytesToStr(bytesBody) + bodyContent, _ := ioutil.ReadAll(req.Body) + req.Body = utils.NewReadCloser(bodyContent, true) + return gconv.UnsafeBytesToStr(bodyContent) } // getResponseBody returns the text of the response body. -func getResponseBody(resp *http.Response) string { - contentType := resp.Header.Get("Content-Type") - if !ifDumpBody(contentType) { +func getResponseBody(res *http.Response) string { + if res.Body == nil { return "" } - bytesBody, errReadBody := ioutil.ReadAll(resp.Body) - if errReadBody != nil { - return "" - } - // So the response body can be read again. - resp.Body = ioutil.NopCloser(bytes.NewBuffer(bytesBody)) - return gconv.UnsafeBytesToStr(bytesBody) + bodyContent, _ := ioutil.ReadAll(res.Body) + res.Body = utils.NewReadCloser(bodyContent, true) + return gconv.UnsafeBytesToStr(bodyContent) } // RawRequest returns the raw content of the request. @@ -116,3 +89,8 @@ func (r *ClientResponse) RawResponse() string { func (r *ClientResponse) Raw() string { return fmt.Sprintf("%s\n%s", r.RawRequest(), r.RawResponse()) } + +// Dump outputs the raw text of the request and the response to stdout. +func (r *ClientResponse) Dump() { + fmt.Println(r.Raw()) +} diff --git a/net/ghttp/ghttp_client_request.go b/net/ghttp/ghttp_client_request.go index 38316dfc5..e37a6c684 100644 --- a/net/ghttp/ghttp_client_request.go +++ b/net/ghttp/ghttp_client_request.go @@ -11,7 +11,9 @@ import ( "encoding/json" "errors" "fmt" + "github.com/gogf/gf/internal/utils" "io" + "io/ioutil" "mime/multipart" "net/http" "os" @@ -215,20 +217,25 @@ func (c *Client) DoRequest(method, url string, data ...interface{}) (resp *Clien if len(c.authUser) > 0 { req.SetBasicAuth(c.authUser, c.authPass) } - // do not return nil even if the request fails - resp = &ClientResponse{} + resp = &ClientResponse{ + request: req, + } + // The request body can be reused for dumping + // raw HTTP request-response procedure. + reqBodyContent, _ := ioutil.ReadAll(req.Body) + req.Body = utils.NewReadCloser(reqBodyContent, false) + defer func() { + resp.request.Body = utils.NewReadCloser(reqBodyContent, true) + }() for { if resp.Response, err = c.Do(req); err != nil { if c.retryCount > 0 { c.retryCount-- time.Sleep(c.retryInterval) } else { - // we need a copy of the request when the request fails. - resp.request = req return resp, err } } else { - resp.request = resp.Request break } } diff --git a/net/ghttp/ghttp_request_body.go b/net/ghttp/ghttp_request_body.go deleted file mode 100644 index c8e6c5b81..000000000 --- a/net/ghttp/ghttp_request_body.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2018 gf Author(https://github.com/gogf/gf). All Rights Reserved. -// -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, -// You can obtain one at https://github.com/gogf/gf. - -package ghttp - -import ( - "bytes" - "io/ioutil" -) - -// BodyReadCloser implements the io.ReadCloser interface -// which is used for reading request body content multiple times. -type BodyReadCloser struct { - *bytes.Reader -} - -// RefillBody refills the request body object after read all of its content. -// It makes the request body reusable for next reading. -func (r *Request) RefillBody() { - if r.bodyContent == nil { - r.bodyContent, _ = ioutil.ReadAll(r.Body) - } - r.Body = &BodyReadCloser{ - bytes.NewReader(r.bodyContent), - } -} - -// Close implements the io.ReadCloser interface. -func (b *BodyReadCloser) Close() error { - return nil -} diff --git a/net/ghttp/ghttp_request_param.go b/net/ghttp/ghttp_request_param.go index 1140fc8d1..f5463cfd2 100644 --- a/net/ghttp/ghttp_request_param.go +++ b/net/ghttp/ghttp_request_param.go @@ -14,6 +14,7 @@ import ( "github.com/gogf/gf/encoding/gjson" "github.com/gogf/gf/encoding/gurl" "github.com/gogf/gf/encoding/gxml" + "github.com/gogf/gf/internal/utils" "github.com/gogf/gf/text/gregex" "github.com/gogf/gf/text/gstr" "github.com/gogf/gf/util/gconv" @@ -121,7 +122,7 @@ func (r *Request) GetRawString() string { func (r *Request) GetBody() []byte { if r.bodyContent == nil { r.bodyContent, _ = ioutil.ReadAll(r.Body) - r.RefillBody() + r.Body = utils.NewReadCloser(r.bodyContent, true) } return r.bodyContent } diff --git a/net/ghttp/ghttp_unit_client_dump_test.go b/net/ghttp/ghttp_unit_client_dump_test.go index d95699a26..a800be65f 100644 --- a/net/ghttp/ghttp_unit_client_dump_test.go +++ b/net/ghttp/ghttp_unit_client_dump_test.go @@ -48,7 +48,7 @@ func Test_Client_Request_13_Dump(t *testing.T) { r2, err := client2.Post("/hello2", g.Map{"field": "test_for_request_body"}) t.Assert(err, nil) dumpedText3 := r2.RawRequest() - t.Assert(gstr.Contains(dumpedText3, "test_for_request_body"), false) + t.Assert(gstr.Contains(dumpedText3, "test_for_request_body"), true) dumpedText4 := r2.RawResponse() t.Assert(gstr.Contains(dumpedText4, "test_for_request_body"), false)