diff --git a/client.go b/client.go index fcd0e1d5..51b98eed 100644 --- a/client.go +++ b/client.go @@ -177,6 +177,7 @@ type Client struct { retryResetReaders bool headerAuthorizationKey string responseBodyLimit int64 + resBodyUnlimitedReads bool jsonEscapeHTML bool setContentLength bool closeConnection bool @@ -563,24 +564,26 @@ func (c *Client) R() *Request { c.lock.RLock() defer c.lock.RUnlock() r := &Request{ - QueryParams: url.Values{}, - FormData: url.Values{}, - Header: http.Header{}, - Cookies: make([]*http.Cookie, 0), - PathParams: make(map[string]string), - RawPathParams: make(map[string]string), - Debug: c.debug, - AuthScheme: c.authScheme, - AuthToken: c.authToken, - UserInfo: c.userInfo, - RetryCount: c.retryCount, - RetryWaitTime: c.retryWaitTime, - RetryMaxWaitTime: c.retryMaxWaitTime, - RetryResetReaders: c.retryResetReaders, - CloseConnection: c.closeConnection, - DoNotParseResponse: c.notParseResponse, - DebugBodyLimit: c.debugBodyLimit, - ResponseBodyLimit: c.responseBodyLimit, + QueryParams: url.Values{}, + FormData: url.Values{}, + Header: http.Header{}, + Cookies: make([]*http.Cookie, 0), + PathParams: make(map[string]string), + RawPathParams: make(map[string]string), + Debug: c.debug, + IsTrace: c.isTrace, + AuthScheme: c.authScheme, + AuthToken: c.authToken, + UserInfo: c.userInfo, + RetryCount: c.retryCount, + RetryWaitTime: c.retryWaitTime, + RetryMaxWaitTime: c.retryMaxWaitTime, + RetryResetReaders: c.retryResetReaders, + CloseConnection: c.closeConnection, + DoNotParseResponse: c.notParseResponse, + DebugBodyLimit: c.debugBodyLimit, + ResponseBodyLimit: c.responseBodyLimit, + ResponseBodyUnlimitedReads: c.resBodyUnlimitedReads, client: c, multipartFiles: []*File{}, @@ -588,7 +591,6 @@ func (c *Client) R() *Request { jsonEscapeHTML: c.jsonEscapeHTML, log: c.log, setContentLength: c.setContentLength, - IsTrace: c.isTrace, generateCurlOnDebug: c.generateCurlOnDebug, } @@ -781,6 +783,18 @@ func (c *Client) IsDebug() bool { return c.debug } +// EnableDebug method is a helper method for [Client.SetDebug] +func (c *Client) EnableDebug() *Client { + c.SetDebug(true) + return c +} + +// DisableDebug method is a helper method for [Client.SetDebug] +func (c *Client) DisableDebug() *Client { + c.SetDebug(false) + return c +} + // SetDebug method enables the debug mode on the Resty client. The client logs details // of every request and response. // @@ -1609,6 +1623,30 @@ func (c *Client) SetGenerateCurlOnDebug(b bool) *Client { return c } +// ResponseBodyUnlimitedReads method returns true if enabled. Otherwise, it returns false +func (c *Client) ResponseBodyUnlimitedReads() bool { + c.lock.RLock() + defer c.lock.RUnlock() + return c.resBodyUnlimitedReads +} + +// SetResponseBodyUnlimitedReads method is to turn on/off the response body copy +// that provides an ability to do unlimited reads. +// +// It can be overridden at the request level; see [Request.SetResponseBodyUnlimitedReads] +// +// NOTE: Turning on this feature uses additional memory to store a copy of the response body buffer. +// +// Unlimited reads are possible in a few scenarios, even without enabling this method. +// - When [Client.SetDebug] set to true +// - When [Request.SetResult] or [Request.SetError] methods are not used +func (c *Client) SetResponseBodyUnlimitedReads(b bool) *Client { + c.lock.Lock() + defer c.lock.Unlock() + c.resBodyUnlimitedReads = b + return c +} + // IsProxySet method returns the true is proxy is set from the Resty client; otherwise // false. By default, the proxy is set from the environment variable; refer to [http.ProxyFromEnvironment]. func (c *Client) IsProxySet() bool { @@ -1639,9 +1677,12 @@ func (c *Client) Clone() *Client { } func (c *Client) executeBefore(req *Request) error { - // Apply Request middleware var err error + if isStringEmpty(req.Method) { + req.Method = MethodGet + } + // user defined on before request methods // to modify the *resty.Request object for _, f := range c.beforeRequestMiddlewares() { @@ -1676,11 +1717,6 @@ func (c *Client) executeBefore(req *Request) error { } } - if err = requestLogger(c, req); err != nil { - return wrapNoRetryErr(err) - } - - req.RawRequest.Body = newRequestBodyReleaser(req.RawRequest.Body, req.bodyBuf) return nil } @@ -1691,27 +1727,39 @@ func (c *Client) execute(req *Request) (*Response, error) { return nil, err } + if err := requestDebugLogger(c, req); err != nil { + return nil, wrapNoRetryErr(err) + } + + req.RawRequest.Body = wrapRequestBufferReleaser(req) req.Time = time.Now() resp, err := c.Client().Do(req.RawRequest) - response := &Response{ - Request: req, - RawResponse: resp, - } - + response := &Response{Request: req, RawResponse: resp} response.setReceivedAt() + if err != nil { + return response, err + } if resp != nil { - response.Body = &limitReadCloser{ - r: resp.Body, + response.Body = &limitReadCloser{r: resp.Body, l: req.ResponseBodyLimit, - f: func(s int64) { - response.size = s - }, + f: func(s int64) { response.size = s }, + } + } + if !req.DoNotParseResponse && (req.Debug || req.ResponseBodyUnlimitedReads) { + response.wrapReadCopier() + + if err := response.readAllBytes(); err != nil { + return response, err } } - if err != nil || req.DoNotParseResponse { // error or do not parse response - return response, wrapErrors(responseLogger(c, response), err) + if err := responseDebugLogger(c, response); err != nil { + return response, err + } + + if req.DoNotParseResponse { + return response, err } // Apply Response middleware @@ -1720,11 +1768,6 @@ func (c *Client) execute(req *Request) (*Response, error) { return response, err } } - // TODO figure out debug response logger with body copy, etc. - err = responseLogger(c, response) - if err != nil { - return response, wrapNoRetryErr(err) - } return response, wrapNoRetryErr(err) } diff --git a/client_test.go b/client_test.go index 90aa876a..b5b4b21a 100644 --- a/client_test.go +++ b/client_test.go @@ -29,7 +29,7 @@ func TestClientBasicAuth(t *testing.T) { ts := createAuthServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetBasicAuth("myuser", "basicauth"). SetBaseURL(ts.URL). SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) @@ -49,7 +49,7 @@ func TestClientAuthToken(t *testing.T) { ts := createAuthServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}). SetAuthToken("004DDB79-6801-4587-B976-F093E6AC44FF"). SetBaseURL(ts.URL + "/") @@ -64,7 +64,7 @@ func TestClientAuthScheme(t *testing.T) { ts := createAuthServer(t) defer ts.Close() - c := dc() + c := dcnl() // Ensure default Bearer c.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}). SetAuthToken("004DDB79-6801-4587-B976-F093E6AC44FF"). @@ -90,7 +90,7 @@ func TestClientDigestAuth(t *testing.T) { ts := createDigestServer(t, conf) defer ts.Close() - c := dc(). + c := dcnl(). SetBaseURL(ts.URL+"/"). SetDigestAuth(conf.username, conf.password) @@ -111,7 +111,7 @@ func TestClientDigestSession(t *testing.T) { ts := createDigestServer(t, conf) defer ts.Close() - c := dc(). + c := dcnl(). SetBaseURL(ts.URL+"/"). SetDigestAuth(conf.username, conf.password) @@ -148,7 +148,7 @@ func TestClientDigestErrors(t *testing.T) { tc.mutateConf(conf) ts := createDigestServer(t, conf) - c := dc(). + c := dcnl(). SetBaseURL(ts.URL+"/"). SetDigestAuth(conf.username, conf.password) @@ -162,7 +162,7 @@ func TestOnAfterMiddleware(t *testing.T) { ts := createGenericServer(t) defer ts.Close() - c := dc() + c := dcnl() c.OnAfterResponse(func(c *Client, res *Response) error { t.Logf("Request sent at: %v", res.Request.Time) t.Logf("Response Received at: %v", res.ReceivedAt()) @@ -183,7 +183,7 @@ func TestClientRedirectPolicy(t *testing.T) { ts := createRedirectServer(t) defer ts.Close() - c := dc().SetRedirectPolicy(FlexibleRedirectPolicy(20)) + c := dcnl().SetRedirectPolicy(FlexibleRedirectPolicy(20)) _, err := c.R().Get(ts.URL + "/redirect-1") assertEqual(t, true, (err.Error() == "Get /redirect-21: stopped after 20 redirects" || @@ -199,7 +199,7 @@ func TestClientTimeout(t *testing.T) { ts := createGetServer(t) defer ts.Close() - c := dc().SetTimeout(time.Second * 3) + c := dcnl().SetTimeout(time.Second * 3) _, err := c.R().Get(ts.URL + "/set-timeout-test") assertEqual(t, true, strings.Contains(strings.ToLower(err.Error()), "timeout")) @@ -209,7 +209,7 @@ func TestClientTimeoutWithinThreshold(t *testing.T) { ts := createGetServer(t) defer ts.Close() - c := dc().SetTimeout(time.Second * 3) + c := dcnl().SetTimeout(time.Second * 3) resp, err := c.R().Get(ts.URL + "/set-timeout-test-with-sequence") assertError(t, err) @@ -225,7 +225,7 @@ func TestClientTimeoutWithinThreshold(t *testing.T) { } func TestClientTimeoutInternalError(t *testing.T) { - c := dc().SetTimeout(time.Second * 1) + c := dcnl().SetTimeout(time.Second * 1) _, _ = c.R().Get("http://localhost:9000/set-timeout-test") } @@ -233,7 +233,7 @@ func TestClientProxy(t *testing.T) { ts := createGetServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetTimeout(1 * time.Second) c.SetProxy("http://sampleproxy:8888") @@ -251,7 +251,7 @@ func TestClientProxy(t *testing.T) { } func TestClientSetCertificates(t *testing.T) { - client := dc() + client := dcnl() client.SetCertificates(tls.Certificate{}) transport, err := client.Transport() @@ -261,57 +261,78 @@ func TestClientSetCertificates(t *testing.T) { } func TestClientSetRootCertificate(t *testing.T) { - client := dc() - client.SetRootCertificate(filepath.Join(getTestDataPath(), "sample-root.pem")) + t.Run("root cert", func(t *testing.T) { + client := dcnl() + client.SetRootCertificate(filepath.Join(getTestDataPath(), "sample-root.pem")) - transport, err := client.Transport() + transport, err := client.Transport() - assertNil(t, err) - assertNotNil(t, transport.TLSClientConfig.RootCAs) -} + assertNil(t, err) + assertNotNil(t, transport.TLSClientConfig.RootCAs) + }) -func TestClientSetRootCertificateNotExists(t *testing.T) { - client := dc() - client.SetRootCertificate(filepath.Join(getTestDataPath(), "not-exists-sample-root.pem")) + t.Run("root cert not exists", func(t *testing.T) { + client := dcnl() + client.SetRootCertificate(filepath.Join(getTestDataPath(), "not-exists-sample-root.pem")) - transport, err := client.Transport() + transport, err := client.Transport() - assertNil(t, err) - assertNil(t, transport.TLSClientConfig) -} + assertNil(t, err) + assertNil(t, transport.TLSClientConfig) + }) -func TestClientSetRootCertificateFromString(t *testing.T) { - client := dc() - rootPemData, err := os.ReadFile(filepath.Join(getTestDataPath(), "sample-root.pem")) - assertNil(t, err) + t.Run("root cert from string", func(t *testing.T) { + client := dcnl() + rootPemData, err := os.ReadFile(filepath.Join(getTestDataPath(), "sample-root.pem")) + assertNil(t, err) - client.SetRootCertificateFromString(string(rootPemData)) + client.SetRootCertificateFromString(string(rootPemData)) - transport, err := client.Transport() + transport, err := client.Transport() - assertNil(t, err) - assertNotNil(t, transport.TLSClientConfig.RootCAs) + assertNil(t, err) + assertNotNil(t, transport.TLSClientConfig.RootCAs) + }) } -func TestClientSetRootCertificateFromStringErrorTls(t *testing.T) { - client := NewWithClient(&http.Client{}) - client.outputLogTo(io.Discard) +func TestClientCACertificateFromStringErrorTls(t *testing.T) { + t.Run("root cert string", func(t *testing.T) { + client := NewWithClient(&http.Client{}) + client.outputLogTo(io.Discard) - rootPemData, err := os.ReadFile(filepath.Join(getTestDataPath(), "sample-root.pem")) - assertNil(t, err) - rt := &CustomRoundTripper{} - client.SetTransport(rt) - transport, err := client.Transport() + rootPemData, err := os.ReadFile(filepath.Join(getTestDataPath(), "sample-root.pem")) + assertNil(t, err) + rt := &CustomRoundTripper{} + client.SetTransport(rt) + transport, err := client.Transport() - client.SetRootCertificateFromString(string(rootPemData)) + client.SetRootCertificateFromString(string(rootPemData)) - assertNotNil(t, rt) - assertNotNil(t, err) - assertNil(t, transport) + assertNotNil(t, rt) + assertNotNil(t, err) + assertNil(t, transport) + }) + + t.Run("client cert string", func(t *testing.T) { + client := NewWithClient(&http.Client{}) + client.outputLogTo(io.Discard) + + rootPemData, err := os.ReadFile(filepath.Join(getTestDataPath(), "sample-root.pem")) + assertNil(t, err) + rt := &CustomRoundTripper{} + client.SetTransport(rt) + transport, err := client.Transport() + + client.SetClientRootCertificateFromString(string(rootPemData)) + + assertNotNil(t, rt) + assertNotNil(t, err) + assertNil(t, transport) + }) } func TestClientSetClientRootCertificate(t *testing.T) { - client := dc() + client := dcnl() client.SetClientRootCertificate(filepath.Join(getTestDataPath(), "sample-root.pem")) transport, err := client.Transport() @@ -321,7 +342,7 @@ func TestClientSetClientRootCertificate(t *testing.T) { } func TestClientSetClientRootCertificateNotExists(t *testing.T) { - client := dc() + client := dcnl() client.SetClientRootCertificate(filepath.Join(getTestDataPath(), "not-exists-sample-root.pem")) transport, err := client.Transport() @@ -331,7 +352,7 @@ func TestClientSetClientRootCertificateNotExists(t *testing.T) { } func TestClientSetClientRootCertificateFromString(t *testing.T) { - client := dc() + client := dcnl() rootPemData, err := os.ReadFile(filepath.Join(getTestDataPath(), "sample-root.pem")) assertNil(t, err) @@ -343,25 +364,8 @@ func TestClientSetClientRootCertificateFromString(t *testing.T) { assertNotNil(t, transport.TLSClientConfig.ClientCAs) } -func TestClientSetClientRootCertificateFromStringErrorTls(t *testing.T) { - client := NewWithClient(&http.Client{}) - client.outputLogTo(io.Discard) - - rootPemData, err := os.ReadFile(filepath.Join(getTestDataPath(), "sample-root.pem")) - assertNil(t, err) - rt := &CustomRoundTripper{} - client.SetTransport(rt) - transport, err := client.Transport() - - client.SetClientRootCertificateFromString(string(rootPemData)) - - assertNotNil(t, rt) - assertNotNil(t, err) - assertNil(t, transport) -} - func TestClientOnBeforeRequestModification(t *testing.T) { - tc := dc() + tc := dcnl() tc.OnBeforeRequest(func(c *Client, r *Request) error { r.SetAuthToken("This is test auth token") return nil @@ -385,7 +389,7 @@ func TestClientSetHeaderVerbatim(t *testing.T) { ts := createPostServer(t) defer ts.Close() - c := dc(). + c := dcnl(). SetHeaderVerbatim("header-lowercase", "value_lowercase"). SetHeader("header-lowercase", "value_standard") @@ -398,7 +402,7 @@ func TestClientSetHeaderVerbatim(t *testing.T) { func TestClientSetTransport(t *testing.T) { ts := createGetServer(t) defer ts.Close() - client := dc() + client := dcnl() transport := &http.Transport{ // something like Proxying to httptest.Server, etc... @@ -414,7 +418,7 @@ func TestClientSetTransport(t *testing.T) { } func TestClientSetScheme(t *testing.T) { - client := dc() + client := dcnl() client.SetScheme("http") @@ -422,7 +426,7 @@ func TestClientSetScheme(t *testing.T) { } func TestClientSetCookieJar(t *testing.T) { - client := dc() + client := dcnl() backupJar := client.httpClient.Jar client.SetCookieJar(nil) @@ -435,7 +439,7 @@ func TestClientSetCookieJar(t *testing.T) { // This test methods exist for test coverage purpose // to validate the getter and setter func TestClientSettingsCoverage(t *testing.T) { - c := dc() + c := dcnl() assertNotNil(t, c.CookieJar()) assertNotNil(t, c.ContentTypeEncoders()) @@ -458,8 +462,10 @@ func TestClientSettingsCoverage(t *testing.T) { c.SetCloseConnection(true) + c.DisableDebug() + // [Start] Custom Transport scenario - ct := dc() + ct := dcnl() ct.SetTransport(&CustomRoundTripper{}) _, err := ct.Transport() assertNotNil(t, err) @@ -470,13 +476,14 @@ func TestClientSettingsCoverage(t *testing.T) { ct.SetCertificates(tls.Certificate{}) ct.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) + ct.SetRootCertificateFromString("root cert") ct.outputLogTo(io.Discard) // [End] Custom Transport scenario } func TestContentLengthWhenBodyIsNil(t *testing.T) { - client := dc() + client := dcnl() client.SetPreRequestHook(func(c *Client, r *http.Request) error { assertEqual(t, "0", r.Header.Get(hdrContentLengthKey)) @@ -487,7 +494,7 @@ func TestContentLengthWhenBodyIsNil(t *testing.T) { } func TestClientPreRequestHook(t *testing.T) { - client := dc() + client := dcnl() client.SetPreRequestHook(func(c *Client, r *http.Request) error { c.log.Debugf("I'm in Pre-Request Hook") return nil @@ -529,7 +536,7 @@ func TestClientAllowsGetMethodPayload(t *testing.T) { ts := createGetServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetAllowGetMethodPayload(true) c.SetPreRequestHook(func(*Client, *http.Request) error { return nil }) // for coverage @@ -545,7 +552,7 @@ func TestClientAllowsGetMethodPayloadIoReader(t *testing.T) { ts := createGetServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetAllowGetMethodPayload(true) payload := "test-payload" @@ -561,7 +568,7 @@ func TestClientAllowsGetMethodPayloadDisabled(t *testing.T) { ts := createGetServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetAllowGetMethodPayload(false) payload := bytes.NewReader([]byte("test-payload")) @@ -595,36 +602,36 @@ func TestDebugBodySizeLimit(t *testing.T) { ts := createGetServer(t) defer ts.Close() - c := dc(). - SetDebug(true). - SetDebugBodyLimit(30) - - var lgr bytes.Buffer - c.outputLogTo(&lgr) // internal method + c, lb := dcld() + c.SetDebugBodyLimit(30) - testcases := []struct{ url, want string }{ + testcases := []struct{ url, want, wantErr string }{ // Text, does not exceed limit. - {ts.URL, "TestGet: text response"}, + {url: ts.URL, want: "TestGet: text response"}, // Empty response. - {ts.URL + "/no-content", "***** NO CONTENT *****"}, + {url: ts.URL + "/no-content", want: "***** NO CONTENT *****"}, // JSON, does not exceed limit. - {ts.URL + "/json", "{\n \"TestGet\": \"JSON response\"\n}"}, + {url: ts.URL + "/json", want: "{\n \"TestGet\": \"JSON response\"\n}"}, // Invalid JSON, does not exceed limit. - {ts.URL + "/json-invalid", "TestGet: Invalid JSON"}, + {url: ts.URL + "/json-invalid", wantErr: "invalid character 'T' looking for beginning of value"}, // Text, exceeds limit. - {ts.URL + "/long-text", "RESPONSE TOO LARGE"}, + {url: ts.URL + "/long-text", want: "RESPONSE TOO LARGE"}, // JSON, exceeds limit. - {ts.URL + "/long-json", "RESPONSE TOO LARGE"}, + {url: ts.URL + "/long-json", want: "RESPONSE TOO LARGE"}, } - for _, tc := range testcases { _, err := c.R().Get(tc.url) - assertError(t, err) - debugLog := lgr.String() - if !strings.Contains(debugLog, tc.want) { - t.Errorf("Expected logs to contain [%v], got [\n%v]", tc.want, debugLog) + if tc.wantErr != "" { + assertNotNil(t, err) + assertEqual(t, tc.wantErr, err.Error()) + } else if tc.want != "" { + assertError(t, err) + debugLog := lb.String() + if !strings.Contains(debugLog, tc.want) { + t.Errorf("Expected logs to contain [%v], got [\n%v]", tc.want, debugLog) + } + lb.Reset() } - lgr.Reset() } } @@ -666,10 +673,7 @@ func TestLogCallbacks(t *testing.T) { ts := createAuthServer(t) defer ts.Close() - c := New().SetDebug(true) - - var lgr bytes.Buffer - c.outputLogTo(&lgr) + c, lb := dcld() c.OnRequestLog(func(r *RequestLog) error { // masking authorization header @@ -693,7 +697,7 @@ func TestLogCallbacks(t *testing.T) { assertEqual(t, http.StatusOK, resp.StatusCode()) // Validating debug log updates - logInfo := lgr.String() + logInfo := lb.String() assertEqual(t, true, strings.Contains(logInfo, "Bearer *******************************")) assertEqual(t, true, strings.Contains(logInfo, "X-Debug-Response-Log")) assertEqual(t, true, strings.Contains(logInfo, "Modified the response body content")) @@ -719,10 +723,9 @@ func TestLogCallbacks(t *testing.T) { func TestDebugLogSimultaneously(t *testing.T) { ts := createGetServer(t) - c := New(). + c := dcnl(). SetDebug(true). - SetBaseURL(ts.URL). - outputLogTo(io.Discard) + SetBaseURL(ts.URL) t.Cleanup(ts.Close) for i := 0; i < 50; i++ { @@ -921,7 +924,7 @@ func TestClientOnResponseError(t *testing.T) { assertEqual(t, 1, hook6) } }() - c := New().outputLogTo(io.Discard). + c := dcnl(). SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}). SetAuthToken("004DDB79-6801-4587-B976-F093E6AC44FF"). SetRetryCount(0). @@ -1009,7 +1012,7 @@ func TestHostURLForGH318AndGH407(t *testing.T) { // test the functionality with httpbin.org locally // will figure out later - c := dc() + c := dcnl() // c.SetScheme("http") // c.SetHostURL(targetURL.Host + "/") @@ -1040,7 +1043,7 @@ func TestPostRedirectWithBody(t *testing.T) { t.Log("ts.URL:", ts.URL) t.Log("targetURL.Host:", targetURL.Host) - c := dc() + c := dcnl() wg := sync.WaitGroup{} for i := 0; i < 100; i++ { wg.Add(1) @@ -1115,7 +1118,7 @@ func TestResponseBodyLimit(t *testing.T) { defer ts.Close() t.Run("Client body limit", func(t *testing.T) { - c := dc().SetResponseBodyLimit(1024) + c := dcnl().SetResponseBodyLimit(1024) assertEqual(t, int64(1024), c.ResponseBodyLimit()) resp, err := c.R().Get(ts.URL + "/") assertNotNil(t, err) @@ -1124,7 +1127,7 @@ func TestResponseBodyLimit(t *testing.T) { }) t.Run("request body limit", func(t *testing.T) { - c := dc() + c := dcnl() resp, err := c.R().SetResponseBodyLimit(1024).Get(ts.URL + "/") assertNotNil(t, err) @@ -1133,7 +1136,7 @@ func TestResponseBodyLimit(t *testing.T) { }) t.Run("body less than limit", func(t *testing.T) { - c := dc() + c := dcnl() res, err := c.R().SetResponseBodyLimit(800*100 + 10).Get(ts.URL + "/") assertNil(t, err) @@ -1142,7 +1145,7 @@ func TestResponseBodyLimit(t *testing.T) { }) t.Run("no body limit", func(t *testing.T) { - c := dc() + c := dcnl() res, err := c.R().Get(ts.URL + "/") assertNil(t, err) @@ -1158,7 +1161,7 @@ func TestResponseBodyLimit(t *testing.T) { }) defer tse.Close() - c := dc() + c := dcnl() _, err := c.R().SetResponseBodyLimit(10240).Get(tse.URL + "/") assertErrorIs(t, gzip.ErrHeader, err) diff --git a/context_test.go b/context_test.go index 378358a9..54830399 100644 --- a/context_test.go +++ b/context_test.go @@ -20,7 +20,7 @@ func TestSetContext(t *testing.T) { ts := createGetServer(t) defer ts.Close() - resp, err := dc().R(). + resp, err := dcnl().R(). SetContext(context.Background()). Get(ts.URL + "/") @@ -37,7 +37,7 @@ func TestSetContextWithError(t *testing.T) { ts := createGetServer(t) defer ts.Close() - resp, err := dcr(). + resp, err := dcnlr(). SetContext(context.Background()). Get(ts.URL + "/mypage") @@ -71,7 +71,7 @@ func TestSetContextCancel(t *testing.T) { cancel() }() - _, err := dc().R(). + _, err := dcnl().R(). SetContext(ctx). Get(ts.URL + "/") @@ -110,7 +110,7 @@ func TestSetContextCancelRetry(t *testing.T) { cancel() }() - c := dc(). + c := dcnl(). SetTimeout(time.Second * 3). SetRetryCount(3) @@ -157,7 +157,7 @@ func TestSetContextCancelWithError(t *testing.T) { cancel() }() - _, err := dc().R(). + _, err := dcnl().R(). SetContext(ctx). Get(ts.URL + "/") @@ -184,7 +184,7 @@ func TestClientRetryWithSetContext(t *testing.T) { }) defer ts.Close() - c := dc(). + c := dcnl(). SetTimeout(time.Second * 1). SetRetryCount(3) @@ -199,7 +199,7 @@ func TestClientRetryWithSetContext(t *testing.T) { } func TestRequestContext(t *testing.T) { - client := dc() + client := dcnl() r := client.NewRequest() assertNotNil(t, r.Context()) diff --git a/curl_cmd_test.go b/curl_cmd_test.go index 0784fbfb..79754b84 100644 --- a/curl_cmd_test.go +++ b/curl_cmd_test.go @@ -10,7 +10,7 @@ import ( // 1. Generate curl for unexecuted request(dry-run) func TestGenerateUnexecutedCurl(t *testing.T) { - req := dclr(). + req := dcnldr(). SetBody(map[string]string{ "name": "Alex", }). @@ -18,7 +18,8 @@ func TestGenerateUnexecutedCurl(t *testing.T) { []*http.Cookie{ {Name: "count", Value: "1"}, }, - ) + ). + SetMethod(MethodPost) assertEqual(t, "", req.GenerateCurlCommand()) @@ -26,7 +27,7 @@ func TestGenerateUnexecutedCurl(t *testing.T) { req.DisableGenerateCurlOnDebug() if !strings.Contains(curlCmdUnexecuted, "Cookie: count=1") || - !strings.Contains(curlCmdUnexecuted, "curl -X GET") || + !strings.Contains(curlCmdUnexecuted, "curl -X POST") || !strings.Contains(curlCmdUnexecuted, `-d '{"name":"Alex"}'`) { t.Fatal("Incomplete curl:", curlCmdUnexecuted) } else { @@ -43,7 +44,7 @@ func TestGenerateExecutedCurl(t *testing.T) { data := map[string]string{ "name": "Alex", } - c := dcl() + c := dcnl().EnableDebug() req := c.R(). SetBody(data). SetCookies( @@ -114,6 +115,13 @@ func TestDebugModeCurl(t *testing.T) { } } +func TestCurlMiscTestCoverage(t *testing.T) { + cookieStr := dumpCurlCookies([]*http.Cookie{ + {Name: "count", Value: "1"}, + }) + assertEqual(t, "Cookie: count=1", cookieStr) +} + func captureStderr() (getOutput func() string, restore func()) { old := os.Stderr r, w, err := os.Pipe() diff --git a/middleware.go b/middleware.go index 2f16baef..4da98dae 100644 --- a/middleware.go +++ b/middleware.go @@ -212,6 +212,7 @@ func createHTTPRequest(c *Client, r *Request) (err error) { } } else { // fix data race: must deep copy. + // TODO investigate in details and remove this copy line bodyBuf := bytes.NewBuffer(append([]byte{}, r.bodyBuf.Bytes()...)) r.RawRequest, err = http.NewRequest(r.Method, r.URL, bodyBuf) } @@ -247,19 +248,6 @@ func createHTTPRequest(c *Client, r *Request) (err error) { r.RawRequest = r.RawRequest.WithContext(r.ctx) } - bodyCopy, err := getBodyCopy(r) - if err != nil { - return err - } - - // assign get body func for the underlying raw request instance - r.RawRequest.GetBody = func() (io.ReadCloser, error) { - if bodyCopy != nil { - return io.NopCloser(bytes.NewReader(bodyCopy.Bytes())), nil - } - return nil, nil - } - return } @@ -296,49 +284,51 @@ func createCurlCmd(c *Client, r *Request) (err error) { if r.resultCurlCmd == nil { r.resultCurlCmd = new(string) } - *r.resultCurlCmd = buildCurlRequest(r.RawRequest, c.Client().Jar) + *r.resultCurlCmd = buildCurlRequest(r) } return nil } -func requestLogger(c *Client, r *Request) error { - if r.Debug { - rr := r.RawRequest - rh := rr.Header.Clone() - if c.Client().Jar != nil { - for _, cookie := range c.Client().Jar.Cookies(r.RawRequest.URL) { - s := fmt.Sprintf("%s=%s", cookie.Name, cookie.Value) - if c := rh.Get("Cookie"); c != "" { - rh.Set("Cookie", c+"; "+s) - } else { - rh.Set("Cookie", s) - } +func requestDebugLogger(c *Client, r *Request) error { + if !r.Debug { + return nil + } + + rr := r.RawRequest + rh := rr.Header.Clone() + if c.Client().Jar != nil { + for _, cookie := range c.Client().Jar.Cookies(r.RawRequest.URL) { + s := fmt.Sprintf("%s=%s", cookie.Name, cookie.Value) + if c := rh.Get("Cookie"); c != "" { + rh.Set("Cookie", c+"; "+s) + } else { + rh.Set("Cookie", s) } } - rl := &RequestLog{Header: rh, Body: r.fmtBodyString(r.DebugBodyLimit)} - if c.requestLog != nil { - if err := c.requestLog(rl); err != nil { - return err - } + } + rl := &RequestLog{Header: rh, Body: r.fmtBodyString(r.DebugBodyLimit)} + if c.requestLog != nil { + if err := c.requestLog(rl); err != nil { + return err } + } - reqLog := "\n==============================================================================\n" + reqLog := "\n==============================================================================\n" - if r.Debug && r.generateCurlOnDebug { - reqLog += "~~~ REQUEST(CURL) ~~~\n" + - fmt.Sprintf(" %v\n", *r.resultCurlCmd) - } + if r.Debug && r.generateCurlOnDebug { + reqLog += "~~~ REQUEST(CURL) ~~~\n" + + fmt.Sprintf(" %v\n", *r.resultCurlCmd) + } - reqLog += "~~~ REQUEST ~~~\n" + - fmt.Sprintf("%s %s %s\n", r.Method, rr.URL.RequestURI(), rr.Proto) + - fmt.Sprintf("HOST : %s\n", rr.URL.Host) + - fmt.Sprintf("HEADERS:\n%s\n", composeHeaders(rl.Header)) + - fmt.Sprintf("BODY :\n%v\n", rl.Body) + - "------------------------------------------------------------------------------\n" + reqLog += "~~~ REQUEST ~~~\n" + + fmt.Sprintf("%s %s %s\n", r.Method, rr.URL.RequestURI(), rr.Proto) + + fmt.Sprintf("HOST : %s\n", rr.URL.Host) + + fmt.Sprintf("HEADERS:\n%s\n", composeHeaders(rl.Header)) + + fmt.Sprintf("BODY :\n%v\n", rl.Body) + + "------------------------------------------------------------------------------\n" - r.initValuesMap() - r.values[debugRequestLogKey] = reqLog - } + r.initValuesMap() + r.values[debugRequestLogKey] = reqLog return nil } @@ -347,40 +337,47 @@ func requestLogger(c *Client, r *Request) error { // Response Middleware(s) //_______________________________________________________________________ -func responseLogger(c *Client, res *Response) error { - if res.Request.Debug { - rl := &ResponseLog{Header: res.Header().Clone(), Body: res.fmtBodyString(res.Request.DebugBodyLimit)} - if c.responseLog != nil { - c.lock.RLock() - defer c.lock.RUnlock() - if err := c.responseLog(rl); err != nil { - return err - } - } +func responseDebugLogger(c *Client, res *Response) error { + if !res.Request.Debug { + return nil + } - debugLog := res.Request.values[debugRequestLogKey].(string) - debugLog += "~~~ RESPONSE ~~~\n" + - fmt.Sprintf("STATUS : %s\n", res.Status()) + - fmt.Sprintf("PROTO : %s\n", res.RawResponse.Proto) + - fmt.Sprintf("RECEIVED AT : %v\n", res.ReceivedAt().Format(time.RFC3339Nano)) + - fmt.Sprintf("TIME DURATION: %v\n", res.Time()) + - "HEADERS :\n" + - composeHeaders(rl.Header) + "\n" - if res.Request.isSaveResponse { - debugLog += "BODY :\n***** RESPONSE WRITTEN INTO FILE *****\n" - } else { - debugLog += fmt.Sprintf("BODY :\n%v\n", rl.Body) + bodyStr, err := res.fmtBodyString(res.Request.DebugBodyLimit) + if err != nil { + return err + } + + rl := &ResponseLog{Header: res.Header().Clone(), Body: bodyStr} + if c.responseLog != nil { + c.lock.RLock() + defer c.lock.RUnlock() + if err := c.responseLog(rl); err != nil { + return err } - debugLog += "==============================================================================\n" + } - res.Request.log.Debugf("%s", debugLog) + debugLog := res.Request.values[debugRequestLogKey].(string) + debugLog += "~~~ RESPONSE ~~~\n" + + fmt.Sprintf("STATUS : %s\n", res.Status()) + + fmt.Sprintf("PROTO : %s\n", res.RawResponse.Proto) + + fmt.Sprintf("RECEIVED AT : %v\n", res.ReceivedAt().Format(time.RFC3339Nano)) + + fmt.Sprintf("TIME DURATION: %v\n", res.Time()) + + "HEADERS :\n" + + composeHeaders(rl.Header) + "\n" + if res.Request.isSaveResponse { + debugLog += "BODY :\n***** RESPONSE WRITTEN INTO FILE *****\n" + } else { + debugLog += fmt.Sprintf("BODY :\n%v\n", rl.Body) } + debugLog += "==============================================================================\n" + + res.Request.log.Debugf("%s", debugLog) return nil } func parseResponseBody(c *Client, res *Response) (err error) { - if res.Request.isSaveResponse { + if res.Request.DoNotParseResponse || res.Request.isSaveResponse { return // move on } @@ -409,6 +406,7 @@ func parseResponseBody(c *Client, res *Response) (err error) { res.Request.Error = nil defer closeq(res.Body) err = decFunc(res.Body, res.Request.Result) + res.IsRead = true return } @@ -422,6 +420,7 @@ func parseResponseBody(c *Client, res *Response) (err error) { if res.Request.Error != nil { defer closeq(res.Body) err = decFunc(res.Body, res.Request.Error) + res.IsRead = true return } } @@ -587,32 +586,3 @@ func saveResponseIntoFile(c *Client, res *Response) error { return nil } - -func getBodyCopy(r *Request) (*bytes.Buffer, error) { - // If r.bodyBuf present, return the copy - if r.bodyBuf != nil { - bodyCopy := acquireBuffer() - if _, err := io.Copy(bodyCopy, bytes.NewReader(r.bodyBuf.Bytes())); err != nil { - // cannot use io.Copy(bodyCopy, r.bodyBuf) because io.Copy reset r.bodyBuf - return nil, err - } - return bodyCopy, nil - } - - // Maybe body is `io.Reader`. - // Note: Resty user have to watchout for large body size of `io.Reader` - if r.RawRequest.Body != nil { - b, err := io.ReadAll(r.RawRequest.Body) - if err != nil { - return nil, err - } - - // Restore the Body - closeq(r.RawRequest.Body) - r.RawRequest.Body = io.NopCloser(bytes.NewBuffer(b)) - - // Return the Body bytes - return bytes.NewBuffer(b), nil - } - return nil, nil -} diff --git a/request.go b/request.go index 491dc977..bda317fe 100644 --- a/request.go +++ b/request.go @@ -27,32 +27,33 @@ import ( // Resty client. The [Request] provides an option to override client-level // settings and also an option for the request composition. type Request struct { - URL string - Method string - AuthToken string - AuthScheme string - QueryParams url.Values - FormData url.Values - PathParams map[string]string - RawPathParams map[string]string - Header http.Header - Time time.Time - Body any - Result any - Error any - RawRequest *http.Request - SRV *SRVRecord - UserInfo *User - Cookies []*http.Cookie - Debug bool - CloseConnection bool - DoNotParseResponse bool - OutputFile string - ExpectResponseContentType string - ForceResponseContentType string - DebugBodyLimit int - ResponseBodyLimit int64 - IsTrace bool + URL string + Method string + AuthToken string + AuthScheme string + QueryParams url.Values + FormData url.Values + PathParams map[string]string + RawPathParams map[string]string + Header http.Header + Time time.Time + Body any + Result any + Error any + RawRequest *http.Request + SRV *SRVRecord + UserInfo *User + Cookies []*http.Cookie + Debug bool + CloseConnection bool + DoNotParseResponse bool + OutputFile string + ExpectResponseContentType string + ForceResponseContentType string + DebugBodyLimit int + ResponseBodyLimit int64 + ResponseBodyUnlimitedReads bool + IsTrace bool // Retry RetryCount int @@ -99,10 +100,16 @@ func (r *Request) GenerateCurlCommand() string { if r.resultCurlCmd == nil { r.resultCurlCmd = new(string) } - *r.resultCurlCmd = buildCurlRequest(r.RawRequest, r.client.httpClient.Jar) + *r.resultCurlCmd = buildCurlRequest(r) return *r.resultCurlCmd } +// SetMethod method used to set the HTTP verb for the request +func (r *Request) SetMethod(m string) *Request { + r.Method = m + return r +} + // Context method returns the Context if it is already set in the [Request] // otherwise, it creates a new one using [context.Background]. func (r *Request) Context() context.Context { @@ -660,6 +667,21 @@ func (r *Request) SetResponseBodyLimit(v int64) *Request { return r } +// SetResponseBodyUnlimitedReads method is to turn on/off the response body copy +// that provides an ability to do unlimited reads. +// +// It overriddes the value set at client level; see [Client.SetResponseBodyUnlimitedReads] +// +// NOTE: Turning on this feature uses additional memory to store a copy of the response body buffer. +// +// Unlimited reads are possible in a few scenarios, even without enabling this method. +// - When [Client.SetDebug] or [Request.SetDebug] set to true +// - When [Request.SetResult] or [Request.SetError] methods are not used +func (r *Request) SetResponseBodyUnlimitedReads(b bool) *Request { + r.ResponseBodyUnlimitedReads = b + return r +} + // SetPathParam method sets a single URL path key-value pair in the // Resty current request instance. // @@ -831,6 +853,18 @@ func (r *Request) SetLogger(l Logger) *Request { return r } +// EnableDebug method is a helper method for [Request.SetDebug] +func (r *Request) EnableDebug() *Request { + r.SetDebug(true) + return r +} + +// DisableDebug method is a helper method for [Request.SetDebug] +func (r *Request) DisableDebug() *Request { + r.SetDebug(false) + return r +} + // SetDebug method enables the debug mode on the current request. It logs // the details current request and response. // @@ -1250,7 +1284,7 @@ func (r *Request) fmtBodyString(sl int) (body string) { (kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) { buf := acquireBuffer() defer releaseBuffer(buf) - if err = encodeJSONEscapeHTMLIndent(buf, r.Body, false, " "); err == nil { + if err = encodeJSONEscapeHTMLIndent(buf, &r.Body, false, " "); err == nil { prtBodyBytes = buf.Bytes() } } else if xmlKey == ctKey && kind == reflect.Struct { diff --git a/request_test.go b/request_test.go index 435943d1..372328c3 100644 --- a/request_test.go +++ b/request_test.go @@ -36,7 +36,7 @@ func TestGet(t *testing.T) { ts := createGetServer(t) defer ts.Close() - resp, err := dc().R(). + resp, err := dcnl().R(). SetQueryParam("request_no", strconv.FormatInt(time.Now().Unix(), 10)). Get(ts.URL + "/") @@ -54,7 +54,7 @@ func TestGetGH524(t *testing.T) { ts := createGetServer(t) defer ts.Close() - resp, err := dc().R(). + resp, err := dcnl().R(). SetPathParams((map[string]string{ "userId": "sample@sample.com", "subAccountId": "100002", @@ -76,7 +76,7 @@ func TestRateLimiter(t *testing.T) { // Test a burst with a valid capacity and then a consecutive request that must fail. // Allow a rate of 1 every 100 ms but also allow bursts of 10 requests. - client := dc().SetRateLimiter(rate.NewLimiter(rate.Every(100*time.Millisecond), 10)) + client := dcnl().SetRateLimiter(rate.NewLimiter(rate.Every(100*time.Millisecond), 10)) // Execute a burst of 10 requests. for i := 0; i < 10; i++ { @@ -95,7 +95,7 @@ func TestRateLimiter(t *testing.T) { // Test continues request at a valid rate // Allow a rate of 1 every ms with no burst. - client = dc().SetRateLimiter(rate.NewLimiter(rate.Every(1*time.Millisecond), 1)) + client = dcnl().SetRateLimiter(rate.NewLimiter(rate.Every(1*time.Millisecond), 1)) // Sending requests every ms+tiny delta must succeed. for i := 0; i < 100; i++ { @@ -111,7 +111,7 @@ func TestIllegalRetryCount(t *testing.T) { ts := createGetServer(t) defer ts.Close() - resp, err := dc().SetRetryCount(-1).R().Get(ts.URL + "/") + resp, err := dcnl().SetRetryCount(-1).R().Get(ts.URL + "/") assertNil(t, err) assertNil(t, resp) @@ -121,7 +121,7 @@ func TestGetCustomUserAgent(t *testing.T) { ts := createGetServer(t) defer ts.Close() - resp, err := dcr(). + resp, err := dcnlr(). SetHeader(hdrUserAgentKey, "Test Custom User agent"). SetQueryParam("request_no", strconv.FormatInt(time.Now().Unix(), 10)). Get(ts.URL + "/") @@ -139,7 +139,7 @@ func TestGetClientParamRequestParam(t *testing.T) { ts := createGetServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetQueryParam("client_param", "true"). SetQueryParams(map[string]string{"req_1": "jeeva", "req_3": "jeeva3"}). SetDebug(true) @@ -164,7 +164,7 @@ func TestGetRelativePath(t *testing.T) { ts := createGetServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetBaseURL(ts.URL) resp, err := c.R().Get("mypage2") @@ -180,7 +180,7 @@ func TestGet400Error(t *testing.T) { ts := createGetServer(t) defer ts.Close() - resp, err := dcr().Get(ts.URL + "/mypage") + resp, err := dcnlr().Get(ts.URL + "/mypage") assertError(t, err) assertEqual(t, http.StatusBadRequest, resp.StatusCode()) @@ -193,7 +193,7 @@ func TestPostJSONStringSuccess(t *testing.T) { ts := createPostServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetHeader(hdrContentTypeKey, "application/json; charset=utf-8"). SetHeaders(map[string]string{hdrUserAgentKey: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) go-resty v0.1", hdrAcceptKey: "application/json; charset=utf-8"}) @@ -221,7 +221,7 @@ func TestPostJSONBytesSuccess(t *testing.T) { ts := createPostServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetHeader(hdrContentTypeKey, "application/json; charset=utf-8"). SetHeaders(map[string]string{hdrUserAgentKey: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) go-resty v0.7", hdrAcceptKey: "application/json; charset=utf-8"}) @@ -239,7 +239,7 @@ func TestPostJSONBytesIoReader(t *testing.T) { ts := createPostServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetHeader(hdrContentTypeKey, "application/json; charset=utf-8") bodyBytes := []byte(`{"username":"testuser", "password":"testpass"}`) @@ -260,7 +260,7 @@ func TestPostJSONStructSuccess(t *testing.T) { user := &User{Username: "testuser", Password: "testpass"} - c := dc().SetJSONEscapeHTML(false) + c := dcnl().SetJSONEscapeHTML(false) resp, err := c.R(). SetHeader(hdrContentTypeKey, "application/json; charset=utf-8"). SetBody(user). @@ -282,7 +282,7 @@ func TestPostJSONRPCStructSuccess(t *testing.T) { user := &User{Username: "testuser", Password: "testpass"} - c := dc().SetJSONEscapeHTML(false) + c := dcnl().SetJSONEscapeHTML(false) resp, err := c.R(). SetHeader(hdrContentTypeKey, "application/json-rpc"). SetBody(user). @@ -303,7 +303,7 @@ func TestPostJSONStructInvalidLogin(t *testing.T) { ts := createPostServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetDebug(false) resp, err := c.R(). @@ -328,7 +328,7 @@ func TestPostJSONErrorRFC7807(t *testing.T) { ts := createPostServer(t) defer ts.Close() - c := dc() + c := dcnl() resp, err := c.R(). SetHeader(hdrContentTypeKey, "application/json; charset=utf-8"). SetBody(User{Username: "testuser", Password: "testpass1"}). @@ -350,7 +350,7 @@ func TestPostJSONMapSuccess(t *testing.T) { ts := createPostServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetDebug(false) resp, err := c.R(). @@ -370,7 +370,7 @@ func TestPostJSONMapInvalidResponseJson(t *testing.T) { ts := createPostServer(t) defer ts.Close() - resp, err := dclr(). + resp, err := dcnldr(). SetBody(map[string]any{"username": "testuser", "password": "invalidjson"}). SetResult(&AuthSuccess{}). Post(ts.URL + "/login") @@ -400,7 +400,7 @@ func TestPostJSONMarshalError(t *testing.T) { b := brokenMarshalJSON{} exp := "b0rk3d" - _, err := dclr(). + _, err := dcnldr(). SetHeader(hdrContentTypeKey, "application/json"). SetBody(b). Post(ts.URL + "/login") @@ -418,7 +418,7 @@ func TestForceContentTypeForGH276andGH240(t *testing.T) { defer ts.Close() retried := 0 - c := dc() + c := dcnl() c.SetDebug(false) c.SetRetryCount(3) c.SetRetryAfter(RetryAfterFunc(func(*Client, *Response) (time.Duration, error) { @@ -446,7 +446,7 @@ func TestPostXMLStringSuccess(t *testing.T) { ts := createPostServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetDebug(false) resp, err := c.R(). @@ -475,7 +475,7 @@ func TestPostXMLMarshalError(t *testing.T) { b := brokenMarshalXML{} exp := "b0rk3d" - _, err := dclr(). + _, err := dcnldr(). SetHeader(hdrContentTypeKey, "application/xml"). SetBody(b). Post(ts.URL + "/login") @@ -492,7 +492,7 @@ func TestPostXMLStringError(t *testing.T) { ts := createPostServer(t) defer ts.Close() - resp, err := dclr(). + resp, err := dcnldr(). SetHeader(hdrContentTypeKey, "application/xml"). SetBody(`testusertestpass`). Post(ts.URL + "/login") @@ -508,7 +508,7 @@ func TestPostXMLBytesSuccess(t *testing.T) { ts := createPostServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetDebug(false) resp, err := c.R(). @@ -528,7 +528,7 @@ func TestPostXMLStructSuccess(t *testing.T) { ts := createPostServer(t) defer ts.Close() - resp, err := dclr(). + resp, err := dcnldr(). SetHeader(hdrContentTypeKey, "application/xml"). SetBody(User{Username: "testuser", Password: "testpass"}). SetContentLength(true). @@ -547,7 +547,7 @@ func TestPostXMLStructInvalidLogin(t *testing.T) { ts := createPostServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetError(&AuthError{}) resp, err := c.R(). @@ -568,7 +568,7 @@ func TestPostXMLStructInvalidResponseXml(t *testing.T) { ts := createPostServer(t) defer ts.Close() - resp, err := dclr(). + resp, err := dcnldr(). SetHeader(hdrContentTypeKey, "application/xml"). SetBody(User{Username: "testuser", Password: "invalidxml"}). SetResult(&AuthSuccess{}). @@ -586,7 +586,7 @@ func TestPostXMLMapNotSupported(t *testing.T) { ts := createPostServer(t) defer ts.Close() - _, err := dclr(). + _, err := dcnldr(). SetHeader(hdrContentTypeKey, "application/xml"). SetBody(map[string]any{"Username": "testuser", "Password": "testpass"}). Post(ts.URL + "/login") @@ -598,7 +598,7 @@ func TestRequestBasicAuth(t *testing.T) { ts := createAuthServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetBaseURL(ts.URL). SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) @@ -618,7 +618,7 @@ func TestRequestBasicAuthWithBody(t *testing.T) { ts := createAuthServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetBaseURL(ts.URL). SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) @@ -643,7 +643,7 @@ func TestRequestInsecureBasicAuth(t *testing.T) { logger := createLogger() logger.l.SetOutput(&logBuf) - c := dc() + c := dcnl() c.SetBaseURL(ts.URL) resp, err := c.R(). @@ -665,7 +665,7 @@ func TestRequestBasicAuthFail(t *testing.T) { ts := createAuthServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}). SetError(AuthError{}) @@ -684,7 +684,7 @@ func TestRequestAuthToken(t *testing.T) { ts := createAuthServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}). SetAuthToken("004DDB79-6801-4587-B976-F093E6AC44FF") @@ -700,7 +700,7 @@ func TestRequestAuthScheme(t *testing.T) { ts := createAuthServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}). SetAuthScheme("OAuth"). SetAuthToken("004DDB79-6801-4587-B976-F093E6AC44FF") @@ -719,7 +719,7 @@ func TestRequestDigestAuth(t *testing.T) { ts := createDigestServer(t, nil) defer ts.Close() - resp, err := dclr(). + resp, err := dcnldr(). SetDigestAuth(conf.username, conf.password). SetResult(&AuthSuccess{}). Get(ts.URL + conf.uri) @@ -736,7 +736,7 @@ func TestRequestDigestAuthFail(t *testing.T) { ts := createDigestServer(t, nil) defer ts.Close() - resp, err := dclr(). + resp, err := dcnldr(). SetDigestAuth(conf.username, "wrongPassword"). SetError(AuthError{}). Get(ts.URL + conf.uri) @@ -753,7 +753,7 @@ func TestRequestDigestAuthWithBody(t *testing.T) { ts := createDigestServer(t, nil) defer ts.Close() - resp, err := dclr(). + resp, err := dcnldr(). SetDigestAuth(conf.username, conf.password). SetResult(&AuthSuccess{}). SetHeader(hdrContentTypeKey, "application/json"). @@ -771,7 +771,7 @@ func TestFormData(t *testing.T) { ts := createFormPostServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetFormData(map[string]string{"zip_code": "00000", "city": "Los Angeles"}). SetContentLength(true). SetDebug(true) @@ -795,7 +795,7 @@ func TestMultiValueFormData(t *testing.T) { "search_criteria": []string{"book", "glass", "pencil"}, } - c := dc() + c := dcnl() c.SetContentLength(true).SetDebug(true) c.outputLogTo(io.Discard) @@ -812,7 +812,7 @@ func TestFormDataDisableWarn(t *testing.T) { ts := createFormPostServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetFormData(map[string]string{"zip_code": "00000", "city": "Los Angeles"}). SetContentLength(true). SetDisableWarn(true) @@ -836,7 +836,7 @@ func TestMultiPartUploadFile(t *testing.T) { basePath := getTestDataPath() - c := dc() + c := dcnl() c.SetFormData(map[string]string{"zip_code": "00001", "city": "Los Angeles"}) resp, err := c.R(). @@ -855,7 +855,7 @@ func TestMultiPartUploadFileViaPatch(t *testing.T) { basePath := getTestDataPath() - c := dc() + c := dcnl() c.SetFormData(map[string]string{"zip_code": "00001", "city": "Los Angeles"}) resp, err := c.R(). @@ -874,7 +874,7 @@ func TestMultiPartUploadFileError(t *testing.T) { basePath := getTestDataPath() - c := dc() + c := dcnl() c.SetFormData(map[string]string{"zip_code": "00001", "city": "Los Angeles"}) resp, err := c.R(). @@ -896,7 +896,7 @@ func TestMultiPartUploadFiles(t *testing.T) { basePath := getTestDataPath() - resp, err := dclr(). + resp, err := dcnldr(). SetFormDataFromValues(url.Values{ "first_name": []string{"Jeevanandam"}, "last_name": []string{"M"}, @@ -929,7 +929,7 @@ func TestMultiPartIoReaderFiles(t *testing.T) { } t.Logf("File Info: %v", file.String()) - resp, err := dclr(). + resp, err := dcnldr(). SetFormData(map[string]string{"first_name": "Jeevanandam", "last_name": "M"}). SetFileReader("profile_img", "test-img.png", bytes.NewReader(profileImgBytes)). SetFileReader("notes", "text-file.txt", bytes.NewReader(notesBytes)). @@ -950,13 +950,13 @@ func TestMultiPartUploadFileNotOnGetOrDelete(t *testing.T) { basePath := getTestDataPath() - _, err := dclr(). + _, err := dcnldr(). SetFile("profile_img", filepath.Join(basePath, "test-img.png")). Get(ts.URL + "/upload") assertEqual(t, "multipart content is not allowed in HTTP verb [GET]", err.Error()) - _, err = dclr(). + _, err = dcnldr(). SetFile("profile_img", filepath.Join(basePath, "test-img.png")). Delete(ts.URL + "/upload") @@ -964,7 +964,7 @@ func TestMultiPartUploadFileNotOnGetOrDelete(t *testing.T) { var hook1Count int var hook2Count int - _, err = dc(). + _, err = dcnl(). OnInvalid(func(r *Request, err error) { assertEqual(t, "multipart content is not allowed in HTTP verb [HEAD]", err.Error()) assertNotNil(t, r) @@ -987,7 +987,7 @@ func TestMultiPartUploadFileNotOnGetOrDelete(t *testing.T) { func TestMultiPartFormData(t *testing.T) { ts := createFormPostServer(t) defer ts.Close() - resp, err := dclr(). + resp, err := dcnldr(). SetMultipartFormData(map[string]string{"first_name": "Jeevanandam", "last_name": "M", "zip_code": "00001"}). SetBasicAuth("myuser", "mypass"). Post(ts.URL + "/profile") @@ -1004,7 +1004,7 @@ func TestMultiPartMultipartField(t *testing.T) { jsonBytes := []byte(`{"input": {"name": "Uploaded document", "_filename" : ["file.txt"]}}`) - resp, err := dclr(). + resp, err := dcnldr(). SetFormDataFromValues(url.Values{ "first_name": []string{"Jeevanandam"}, "last_name": []string{"M"}, @@ -1047,7 +1047,7 @@ func TestMultiPartMultipartFields(t *testing.T) { }, } - resp, err := dclr(). + resp, err := dcnldr(). SetFormData(map[string]string{"first_name": "Jeevanandam", "last_name": "M"}). SetMultipartFields(fields...). Post(ts.URL + "/upload") @@ -1065,7 +1065,7 @@ func TestMultiPartCustomBoundary(t *testing.T) { defer ts.Close() defer cleanupFiles(".testdata/upload") - _, err := dclr(). + _, err := dcnldr(). SetMultipartFormData(map[string]string{"first_name": "Jeevanandam", "last_name": "M", "zip_code": "00001"}). SetMultipartBoundary(`"my-custom-boundary"`). SetBasicAuth("myuser", "mypass"). @@ -1073,7 +1073,7 @@ func TestMultiPartCustomBoundary(t *testing.T) { assertEqual(t, "mime: invalid boundary character", err.Error()) - resp, err := dclr(). + resp, err := dcnldr(). SetMultipartFormData(map[string]string{"first_name": "Jeevanandam", "last_name": "M", "zip_code": "00001"}). SetMultipartBoundary("my-custom-boundary"). Post(ts.URL + "/profile") @@ -1087,7 +1087,7 @@ func TestGetWithCookie(t *testing.T) { ts := createGetServer(t) defer ts.Close() - c := dcl() + c := dcnl() c.SetBaseURL(ts.URL) c.SetCookie(&http.Cookie{ Name: "go-resty-1", @@ -1118,7 +1118,7 @@ func TestGetWithCookies(t *testing.T) { ts := createGetServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetBaseURL(ts.URL).SetDebug(true) tu, _ := url.Parse(ts.URL) @@ -1167,7 +1167,7 @@ func TestPutPlainString(t *testing.T) { ts := createGenericServer(t) defer ts.Close() - resp, err := dc().R(). + resp, err := dcnl().R(). SetBody("This is plain text body to server"). Put(ts.URL + "/plaintext") @@ -1180,7 +1180,7 @@ func TestPutJSONString(t *testing.T) { ts := createGenericServer(t) defer ts.Close() - client := dc() + client := dcnl() client.OnBeforeRequest(func(c *Client, r *Request) error { r.SetHeader("X-Custom-Request-Middleware", "OnBeforeRequest middleware") @@ -1209,7 +1209,7 @@ func TestPutXMLString(t *testing.T) { ts := createGenericServer(t) defer ts.Close() - resp, err := dc().R(). + resp, err := dcnl().R(). SetHeaders(map[string]string{hdrContentTypeKey: "application/xml", hdrAcceptKey: "application/xml"}). SetBody(`XML Content sending to server`). Put(ts.URL + "/xml") @@ -1223,7 +1223,7 @@ func TestOnBeforeMiddleware(t *testing.T) { ts := createGenericServer(t) defer ts.Close() - c := dc() + c := dcnl() c.OnBeforeRequest(func(c *Client, r *Request) error { r.SetHeader("X-Custom-Request-Middleware", "OnBeforeRequest middleware") return nil @@ -1247,7 +1247,7 @@ func TestHTTPAutoRedirectUpTo10(t *testing.T) { ts := createRedirectServer(t) defer ts.Close() - _, err := dc().R().Get(ts.URL + "/redirect-1") + _, err := dcnl().R().Get(ts.URL + "/redirect-1") assertEqual(t, true, (err.Error() == "Get /redirect-11: stopped after 10 redirects" || err.Error() == "Get \"/redirect-11\": stopped after 10 redirects")) @@ -1257,7 +1257,7 @@ func TestHostCheckRedirectPolicy(t *testing.T) { ts := createRedirectServer(t) defer ts.Close() - c := dc(). + c := dcnl(). SetRedirectPolicy(DomainCheckRedirectPolicy("127.0.0.1")) _, err := c.R().Get(ts.URL + "/redirect-host-check-1") @@ -1271,14 +1271,14 @@ func TestHttpMethods(t *testing.T) { defer ts.Close() t.Run("head method", func(t *testing.T) { - resp, err := dclr().Head(ts.URL + "/") + resp, err := dcnldr().Head(ts.URL + "/") assertError(t, err) assertEqual(t, http.StatusOK, resp.StatusCode()) }) t.Run("options method", func(t *testing.T) { - resp, err := dclr().Options(ts.URL + "/options") + resp, err := dcnldr().Options(ts.URL + "/options") assertError(t, err) assertEqual(t, http.StatusOK, resp.StatusCode()) @@ -1286,7 +1286,7 @@ func TestHttpMethods(t *testing.T) { }) t.Run("patch method", func(t *testing.T) { - resp, err := dclr().Patch(ts.URL + "/patch") + resp, err := dcnldr().Patch(ts.URL + "/patch") assertError(t, err) assertEqual(t, http.StatusOK, resp.StatusCode()) @@ -1295,7 +1295,7 @@ func TestHttpMethods(t *testing.T) { }) t.Run("trace method", func(t *testing.T) { - resp, err := dclr().Trace(ts.URL + "/trace") + resp, err := dcnldr().Trace(ts.URL + "/trace") assertError(t, err) assertEqual(t, http.StatusOK, resp.StatusCode()) @@ -1304,7 +1304,7 @@ func TestHttpMethods(t *testing.T) { }) t.Run("connect method", func(t *testing.T) { - resp, err := dclr().Connect(ts.URL + "/connect") + resp, err := dcnldr().Connect(ts.URL + "/connect") assertError(t, err) assertEqual(t, http.StatusOK, resp.StatusCode()) @@ -1317,9 +1317,21 @@ func TestSendMethod(t *testing.T) { ts := createGenericServer(t) defer ts.Close() + t.Run("send-get-implicit", func(t *testing.T) { + req := dcnldr() + req.URL = ts.URL + "/gzip-test" + + resp, err := req.Send() + + assertError(t, err) + assertEqual(t, http.StatusOK, resp.StatusCode()) + + assertEqual(t, "This is Gzip response testing", resp.String()) + }) + t.Run("send-get", func(t *testing.T) { - req := dclr() - req.Method = http.MethodGet + req := dcnldr() + req.SetMethod(MethodGet) req.URL = ts.URL + "/gzip-test" resp, err := req.Send() @@ -1331,8 +1343,8 @@ func TestSendMethod(t *testing.T) { }) t.Run("send-options", func(t *testing.T) { - req := dclr() - req.Method = http.MethodOptions + req := dcnldr() + req.SetMethod(MethodOptions) req.URL = ts.URL + "/options" resp, err := req.Send() @@ -1345,8 +1357,8 @@ func TestSendMethod(t *testing.T) { }) t.Run("send-patch", func(t *testing.T) { - req := dclr() - req.Method = http.MethodPatch + req := dcnldr() + req.SetMethod(MethodPatch) req.URL = ts.URL + "/patch" resp, err := req.Send() @@ -1358,8 +1370,8 @@ func TestSendMethod(t *testing.T) { }) t.Run("send-put", func(t *testing.T) { - req := dclr() - req.Method = http.MethodPut + req := dcnldr() + req.SetMethod(MethodPut) req.URL = ts.URL + "/plaintext" resp, err := req.Send() @@ -1378,7 +1390,7 @@ func TestRawFileUploadByBody(t *testing.T) { fileBytes, err := os.ReadFile(filepath.Join(getTestDataPath(), "test-img.png")) assertNil(t, err) - resp, err := dclr(). + resp, err := dcnldr(). SetBody(fileBytes). SetContentLength(true). SetAuthToken("004DDB79-6801-4587-B976-F093E6AC44FF"). @@ -1390,7 +1402,7 @@ func TestRawFileUploadByBody(t *testing.T) { } func TestProxySetting(t *testing.T) { - c := dc() + c := dcnl() transport, err := c.Transport() @@ -1426,7 +1438,7 @@ func TestGetClient(t *testing.T) { } func TestIncorrectURL(t *testing.T) { - c := dc() + c := dcnl() _, err := c.R().Get("//not.a.user@%66%6f%6f.com/just/a/path/also") assertEqual(t, true, (strings.Contains(err.Error(), "parse //not.a.user@%66%6f%6f.com/just/a/path/also") || strings.Contains(err.Error(), "parse \"//not.a.user@%66%6f%6f.com/just/a/path/also\""))) @@ -1443,7 +1455,7 @@ func TestDetectContentTypeForPointer(t *testing.T) { user := &User{Username: "testuser", Password: "testpass"} - resp, err := dclr(). + resp, err := dcnldr(). SetBody(user). SetResult(AuthSuccess{}). Post(ts.URL + "/login") @@ -1472,7 +1484,7 @@ func TestDetectContentTypeForPointerWithSlice(t *testing.T) { {FirstName: "firstname3", LastName: "lastname3", ZipCode: "10003"}, } - resp, err := dclr(). + resp, err := dcnldr(). SetBody(users). Post(ts.URL + "/users") @@ -1497,7 +1509,7 @@ func TestDetectContentTypeForPointerWithSliceMap(t *testing.T) { var users []map[string]any users = append(users, usersmap) - resp, err := dclr(). + resp, err := dcnldr(). SetBody(&users). Post(ts.URL + "/usersmap") @@ -1519,7 +1531,7 @@ func TestDetectContentTypeForSlice(t *testing.T) { {FirstName: "firstname3", LastName: "lastname3", ZipCode: "10003"}, } - resp, err := dclr(). + resp, err := dcnldr(). SetBody(users). Post(ts.URL + "/users") @@ -1535,7 +1547,7 @@ func TestMultiParamsQueryString(t *testing.T) { ts1 := createGetServer(t) defer ts1.Close() - client := dc() + client := dcnl() req1 := client.R() client.SetQueryParam("status", "open") @@ -1577,7 +1589,7 @@ func TestSetQueryStringTypical(t *testing.T) { ts := createGetServer(t) defer ts.Close() - resp, err := dclr(). + resp, err := dcnldr(). SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more"). Get(ts.URL) @@ -1586,7 +1598,7 @@ func TestSetQueryStringTypical(t *testing.T) { assertEqual(t, "200 OK", resp.Status()) assertEqual(t, "TestGet: text response", resp.String()) - resp, err = dclr(). + resp, err = dcnldr(). SetQueryString("&%%amp;"). Get(ts.URL) @@ -1600,7 +1612,7 @@ func TestSetHeaderVerbatim(t *testing.T) { ts := createPostServer(t) defer ts.Close() - r := dclr(). + r := dcnldr(). SetHeaderVerbatim("header-lowercase", "value_lowercase"). SetHeader("header-lowercase", "value_standard") @@ -1613,7 +1625,7 @@ func TestSetHeaderMultipleValue(t *testing.T) { ts := createPostServer(t) defer ts.Close() - r := dclr(). + r := dcnldr(). SetHeaderMultiValues(map[string][]string{ "Content": {"text/*", "text/html", "*"}, "Authorization": {"Bearer xyz"}, @@ -1628,7 +1640,7 @@ func TestOutputFileWithBaseDirAndRelativePath(t *testing.T) { defer cleanupFiles(".testdata/dir-sample") baseOutputDir := filepath.Join(getTestDataPath(), "dir-sample") - client := dc(). + client := dcnl(). SetRedirectPolicy(FlexibleRedirectPolicy(10)). SetOutputDirectory(baseOutputDir). SetDebug(true) @@ -1649,7 +1661,7 @@ func TestOutputFileWithBaseDirAndRelativePath(t *testing.T) { } func TestOutputFileWithBaseDirError(t *testing.T) { - c := dc().SetRedirectPolicy(FlexibleRedirectPolicy(10)). + c := dcnl().SetRedirectPolicy(FlexibleRedirectPolicy(10)). SetOutputDirectory(filepath.Join(getTestDataPath(), `go-resty\0`)) _ = c @@ -1660,7 +1672,7 @@ func TestOutputPathDirNotExists(t *testing.T) { defer ts.Close() defer cleanupFiles(filepath.Join(".testdata", "not-exists-dir")) - client := dc(). + client := dcnl(). SetRedirectPolicy(FlexibleRedirectPolicy(10)). SetOutputDirectory(filepath.Join(getTestDataPath(), "not-exists-dir")) @@ -1678,7 +1690,7 @@ func TestOutputFileAbsPath(t *testing.T) { defer ts.Close() defer cleanupFiles(filepath.Join(".testdata", "go-resty")) - _, err := dcr(). + _, err := dcnlr(). SetOutputFile(filepath.Join(getTestDataPath(), "go-resty", "test-img-success-2.png")). Get(ts.URL + "/my-image.png") @@ -1689,7 +1701,7 @@ func TestContextInternal(t *testing.T) { ts := createGetServer(t) defer ts.Close() - r := dc().R(). + r := dcnl().R(). SetQueryParam("request_no", strconv.FormatInt(time.Now().Unix(), 10)) resp, err := r.Get(ts.URL + "/") @@ -1699,7 +1711,7 @@ func TestContextInternal(t *testing.T) { } func TestSRV(t *testing.T) { - c := dc(). + c := dcnl(). SetRedirectPolicy(FlexibleRedirectPolicy(20)). SetScheme("http") @@ -1718,7 +1730,7 @@ func TestSRV(t *testing.T) { } func TestSRVInvalidService(t *testing.T) { - _, err := dc().R(). + _, err := dcnl().R(). SetSRV(&SRVRecord{"nonexistantservice", "sampledomain"}). Get("/") @@ -1730,30 +1742,31 @@ func TestRequestDoNotParseResponse(t *testing.T) { ts := createGetServer(t) defer ts.Close() - client := dc().SetDoNotParseResponse(true) - resp, err := client.R(). - SetQueryParam("request_no", strconv.FormatInt(time.Now().Unix(), 10)). - Get(ts.URL + "/") - - assertError(t, err) + t.Run("do not parse response 1", func(t *testing.T) { + client := dcnl().SetDoNotParseResponse(true) + resp, err := client.R(). + SetQueryParam("request_no", strconv.FormatInt(time.Now().Unix(), 10)). + Get(ts.URL + "/") - buf := acquireBuffer() - defer releaseBuffer(buf) - _, _ = io.Copy(buf, resp.Body) + assertError(t, err) - assertEqual(t, "TestGet: text response", buf.String()) - _ = resp.Body.Close() + b, err := io.ReadAll(resp.Body) + _ = resp.Body.Close() + assertError(t, err) + assertEqual(t, "TestGet: text response", string(b)) + }) - // Manually setting RawResponse as nil - resp, err = dc().R(). - SetDoNotParseResponse(true). - Get(ts.URL + "/") + t.Run("manual reset raw response - do not parse response 2", func(t *testing.T) { + resp, err := dcnl().R(). + SetDoNotParseResponse(true). + Get(ts.URL + "/") - assertError(t, err) + assertError(t, err) - resp.RawResponse = nil - assertEqual(t, 0, resp.StatusCode()) - assertEqual(t, "", resp.String()) + resp.RawResponse = nil + assertEqual(t, 0, resp.StatusCode()) + assertEqual(t, "", resp.String()) + }) } func TestRequestDoNotParseResponseDebugLog(t *testing.T) { @@ -1761,7 +1774,7 @@ func TestRequestDoNotParseResponseDebugLog(t *testing.T) { defer ts.Close() t.Run("do not parse response debug log client level", func(t *testing.T) { - c := dc(). + c := dcnl(). SetDoNotParseResponse(true). SetDebug(true) @@ -1777,7 +1790,7 @@ func TestRequestDoNotParseResponseDebugLog(t *testing.T) { }) t.Run("do not parse response debug log request level", func(t *testing.T) { - c := dc() + c := dcnl() var lgr bytes.Buffer c.outputLogTo(&lgr) @@ -1801,7 +1814,7 @@ func TestRequestExpectContentTypeTest(t *testing.T) { ts := createGenericServer(t) defer ts.Close() - c := dc() + c := dcnl() resp, err := c.R(). SetResult(noCtTest{}). SetExpectResponseContentType("application/json"). @@ -1819,7 +1832,7 @@ func TestGetPathParamAndPathParams(t *testing.T) { ts := createGetServer(t) defer ts.Close() - c := dc(). + c := dcnl(). SetBaseURL(ts.URL). SetPathParam("userId", "sample@sample.com") @@ -1840,7 +1853,7 @@ func TestReportMethodSupportsPayload(t *testing.T) { ts := createGenericServer(t) defer ts.Close() - c := dc() + c := dcnl() resp, err := c.R(). SetBody("body"). Execute("REPORT", ts.URL+"/report") @@ -1870,7 +1883,7 @@ func TestRequestOverridesClientAuthorizationHeader(t *testing.T) { ts := createAuthServer(t) defer ts.Close() - c := dc() + c := dcnl() c.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}). SetHeader("Authorization", "some token"). SetBaseURL(ts.URL + "/") @@ -1890,7 +1903,7 @@ func TestRequestFileUploadAsReader(t *testing.T) { file, _ := os.Open(filepath.Join(getTestDataPath(), "test-img.png")) defer file.Close() - resp, err := dclr(). + resp, err := dcnldr(). SetBody(file). SetHeader("Content-Type", "image/png"). Post(ts.URL + "/upload") @@ -1902,7 +1915,7 @@ func TestRequestFileUploadAsReader(t *testing.T) { file, _ = os.Open(filepath.Join(getTestDataPath(), "test-img.png")) defer file.Close() - resp, err = dclr(). + resp, err = dcnldr(). SetBody(file). SetHeader("Content-Type", "image/png"). SetContentLength(true). @@ -1917,7 +1930,7 @@ func TestHostHeaderOverride(t *testing.T) { ts := createGetServer(t) defer ts.Close() - resp, err := dc().R(). + resp, err := dcnl().R(). SetHeader("Host", "myhostname"). Get(ts.URL + "/host-header") @@ -1939,7 +1952,7 @@ func TestNotFoundWithError(t *testing.T) { ts := createGetServer(t) defer ts.Close() - resp, err := dc().R(). + resp, err := dcnl().R(). SetHeader(hdrContentTypeKey, "application/json"). SetError(&httpError). Get(ts.URL + "/not-found-with-error") @@ -1959,7 +1972,7 @@ func TestNotFoundWithoutError(t *testing.T) { ts := createGetServer(t) defer ts.Close() - c := dc().outputLogTo(os.Stdout) + c := dcnl().outputLogTo(os.Stdout) resp, err := c.R(). SetError(&httpError). SetHeader(hdrContentTypeKey, "application/json"). @@ -1978,7 +1991,7 @@ func TestPathParamURLInput(t *testing.T) { ts := createGetServer(t) defer ts.Close() - c := dc(). + c := dcnl(). SetBaseURL(ts.URL). SetPathParams(map[string]string{ "userId": "sample@sample.com", @@ -2004,7 +2017,7 @@ func TestRawPathParamURLInput(t *testing.T) { ts := createGetServer(t) defer ts.Close() - c := dc().SetDebug(true). + c := dcnl(). SetBaseURL(ts.URL). SetRawPathParams(map[string]string{ "userId": "sample@sample.com", @@ -2014,7 +2027,7 @@ func TestRawPathParamURLInput(t *testing.T) { assertEqual(t, "sample@sample.com", c.RawPathParams()["userId"]) assertEqual(t, "users/developers", c.RawPathParams()["path"]) - resp, err := c.R(). + resp, err := c.R().EnableDebug(). SetRawPathParams(map[string]string{ "subAccountId": "100002", "website": "https://example.com", @@ -2035,7 +2048,7 @@ func TestTraceInfo(t *testing.T) { serverAddr := ts.URL[strings.LastIndex(ts.URL, "/")+1:] - client := dc() + client := dcnl() client.SetBaseURL(ts.URL).EnableTrace() for _, u := range []string{"/", "/json", "/long-text", "/long-json"} { resp, err := client.R().Get(u) @@ -2080,7 +2093,7 @@ func TestTraceInfoWithoutEnableTrace(t *testing.T) { ts := createGetServer(t) defer ts.Close() - client := dc() + client := dcnl() client.SetBaseURL(ts.URL) for _, u := range []string{"/", "/json", "/long-text", "/long-json"} { resp, err := client.R().Get(u) @@ -2098,7 +2111,7 @@ func TestTraceInfoWithoutEnableTrace(t *testing.T) { } func TestTraceInfoOnTimeout(t *testing.T) { - client := dc() + client := dcnl() client.SetBaseURL("http://resty-nowhere.local").EnableTrace() resp, err := client.R().Get("/") @@ -2211,7 +2224,7 @@ func TestPostMapTemporaryRedirect(t *testing.T) { ts := createPostServer(t) defer ts.Close() - c := dc() + c := dcnl() resp, err := c.R().SetBody(map[string]string{"username": "testuser", "password": "testpass"}). Post(ts.URL + "/redirect") @@ -2220,6 +2233,19 @@ func TestPostMapTemporaryRedirect(t *testing.T) { assertEqual(t, http.StatusOK, resp.StatusCode()) } +func TestPostWith204Responset(t *testing.T) { + ts := createPostServer(t) + defer ts.Close() + + c := dcnl() + resp, err := c.R().SetBody(map[string]string{"username": "testuser", "password": "testpass"}). + Post(ts.URL + "/204-response") + + assertNil(t, err) + assertNotNil(t, resp) + assertEqual(t, http.StatusNoContent, resp.StatusCode()) +} + type brokenReadCloser struct{} func (b brokenReadCloser) Read(p []byte) (n int, err error) { @@ -2234,11 +2260,11 @@ func TestPostBodyError(t *testing.T) { ts := createPostServer(t) defer ts.Close() - c := dc() + c := dcnl() resp, err := c.R().SetBody(brokenReadCloser{}).Post(ts.URL + "/redirect") assertNotNil(t, err) - assertEqual(t, "read error", err.Error()) - assertNil(t, resp) + assertEqual(t, "read error", errors.Unwrap(err).Error()) + assertNotNil(t, resp) } func TestSetResultMustNotPanicOnNil(t *testing.T) { @@ -2247,14 +2273,14 @@ func TestSetResultMustNotPanicOnNil(t *testing.T) { t.Errorf("must not panic") } }() - dc().R().SetResult(nil) + dcnl().R().SetResult(nil) } func TestRequestClone(t *testing.T) { ts := createGetServer(t) defer ts.Close() - c := dc() + c := dcnl() parent := c.R() // set an non-interface value @@ -2317,10 +2343,43 @@ func TestRequestClone(t *testing.T) { assertEqual(t, "xmpp-server-clone", clone.SRV.Service) } +func TestResponseBodyUnlimitedReads(t *testing.T) { + ts := createPostServer(t) + defer ts.Close() + + user := &User{Username: "testuser", Password: "testpass"} + + c := dcnl(). + SetJSONEscapeHTML(false). + SetResponseBodyUnlimitedReads(true) + + assertEqual(t, true, c.ResponseBodyUnlimitedReads()) + + resp, err := c.R(). + SetHeader(hdrContentTypeKey, "application/json; charset=utf-8"). + SetBody(user). + SetResult(&AuthSuccess{}). + Post(ts.URL + "/login") + + assertError(t, err) + assertEqual(t, http.StatusOK, resp.StatusCode()) + assertEqual(t, int64(50), resp.Size()) + + t.Logf("Result Success: %q", resp.Result().(*AuthSuccess)) + + for i := 1; i <= 5; i++ { + b, err := io.ReadAll(resp.Body) + assertNil(t, err) + assertEqual(t, `{ "id": "success", "message": "login successful" }`, string(b)) + } + + logResponse(t, resp) +} + // This test methods exist for test coverage purpose // to validate the getter and setter func TestRequestSettingsCoverage(t *testing.T) { - c := dc() + c := dcnl() c.R().SetCloseConnection(true) @@ -2329,4 +2388,8 @@ func TestRequestSettingsCoverage(t *testing.T) { srv := []*net.SRV{} srv = append(srv, &net.SRV{}) c.R().selectAddr(srv, "/", 1) + + c.R().SetResponseBodyUnlimitedReads(true) + + c.R().DisableDebug() } diff --git a/response.go b/response.go index ca11b1a5..4afa625e 100644 --- a/response.go +++ b/response.go @@ -23,6 +23,7 @@ type Response struct { Request *Request Body io.ReadCloser RawResponse *http.Response + IsRead bool bodyBytes []byte size int64 @@ -35,6 +36,8 @@ type Response struct { // - [Response.BodyBytes] might be `nil` if [Request.SetOutputFile], [Request.SetDoNotParseResponse], // [Client.SetDoNotParseResponse] method is used. // - [Response.BodyBytes] might be `nil` if [Response].Body is already auto-unmarshal performed. +// +// TODO remove it func (r *Response) BodyBytes() []byte { if r.RawResponse == nil { return []byte{} @@ -106,8 +109,8 @@ func (r *Response) Cookies() []*http.Cookie { // NOTE: // - Returns an empty string on auto-unmarshal scenarios func (r *Response) String() string { - if len(r.bodyBytes) == 0 { - return "" + if len(r.bodyBytes) == 0 && !r.Request.DoNotParseResponse { + _ = r.readAllBytes() } return strings.TrimSpace(string(r.bodyBytes)) } @@ -154,35 +157,66 @@ func (r *Response) setReceivedAt() { } } -func (r *Response) fmtBodyString(sl int) string { +func (r *Response) fmtBodyString(sl int) (string, error) { if r.Request.DoNotParseResponse { - return "***** DO NOT PARSE RESPONSE - Enabled *****" + return "***** DO NOT PARSE RESPONSE - Enabled *****", nil + } + + bl := len(r.bodyBytes) + if r.IsRead && bl == 0 { + return "***** RESPONSE BODY IS ALREADY READ - see Response.{Result()/Error()} *****", nil } - if len(r.bodyBytes) > 0 { - if len(r.bodyBytes) > sl { - return fmt.Sprintf("***** RESPONSE TOO LARGE (size - %d) *****", len(r.bodyBytes)) + + if bl > 0 { + if bl > sl { + return fmt.Sprintf("***** RESPONSE TOO LARGE (size - %d) *****", bl), nil } + ct := r.Header().Get(hdrContentTypeKey) - if isJSONContentType(ct) { + ctKey := inferContentTypeMapKey(ct) + if jsonKey == ctKey { out := acquireBuffer() defer releaseBuffer(out) err := json.Indent(out, r.bodyBytes, "", " ") if err != nil { - return fmt.Sprintf("*** Error: Unable to format response body - \"%s\" ***\n\nLog Body as-is:\n%s", err, r.String()) + return "", err } - return out.String() + return out.String(), nil } - return r.String() + return r.String(), nil } - return "***** NO CONTENT *****" + return "***** NO CONTENT *****", nil } // auto-unmarshal didn't happen, so fallback to // old behavior of reading response as body bytes func (r *Response) readAllBytes() (err error) { - defer closeq(r.Body) - r.bodyBytes, err = io.ReadAll(r.Body) - r.Body = io.NopCloser(bytes.NewReader(r.bodyBytes)) + if r.IsRead { + return nil + } + + if _, ok := r.Body.(*readCopier); ok { + _, err = io.ReadAll(r.Body) + } else { + r.bodyBytes, err = io.ReadAll(r.Body) + closeq(r.Body) + r.Body = &readNoOpCloser{r: bytes.NewReader(r.bodyBytes)} + } + + r.IsRead = true return } + +func (r *Response) wrapReadCopier() { + r.Body = &readCopier{ + s: r.Body, + t: acquireBuffer(), + f: func(b *bytes.Buffer) { + r.bodyBytes = append([]byte{}, b.Bytes()...) + closeq(r.Body) + r.Body = &readNoOpCloser{r: bytes.NewReader(r.bodyBytes)} + releaseBuffer(b) + }, + } +} diff --git a/resty_test.go b/resty_test.go index 3d8f62d5..30b54779 100644 --- a/resty_test.go +++ b/resty_test.go @@ -5,6 +5,7 @@ package resty import ( + "bytes" "compress/gzip" "crypto/md5" "encoding/base64" @@ -318,6 +319,8 @@ func createPostServer(t *testing.T) *httptest.Server { } http.SetCookie(w, &cookie) w.WriteHeader(http.StatusOK) + case "/204-response": + w.WriteHeader(http.StatusNoContent) } } }) @@ -817,27 +820,27 @@ func createTestServer(fn func(w http.ResponseWriter, r *http.Request)) *httptest return httptest.NewServer(http.HandlerFunc(fn)) } -func dc() *Client { +func dcnl() *Client { c := New(). outputLogTo(io.Discard) return c } -func dcl() *Client { +func dcld() (*Client, *bytes.Buffer) { + logBuf := acquireBuffer() c := New(). - SetDebug(true). - outputLogTo(io.Discard) - return c + EnableDebug(). + outputLogTo(logBuf) + return c, logBuf } -func dcr() *Request { - return dc().R() +func dcnlr() *Request { + return dcnl().R() } -func dclr() *Request { - c := dc(). - SetDebug(true). - outputLogTo(io.Discard) +func dcnldr() *Request { + c := dcnl(). + SetDebug(true) return c.R() } diff --git a/retry_test.go b/retry_test.go index 75127777..08683018 100644 --- a/retry_test.go +++ b/retry_test.go @@ -143,7 +143,7 @@ func TestConditionalGet(t *testing.T) { return attemptCount != externalCounter }) - client := dc().AddRetryCondition(check).SetRetryCount(1) + client := dcnl().AddRetryCondition(check).SetRetryCount(1) resp, err := client.R(). SetQueryParam("request_no", strconv.FormatInt(time.Now().Unix(), 10)). Get(ts.URL + "/") @@ -171,7 +171,7 @@ func TestConditionalGetRequestLevel(t *testing.T) { }) // Clear the default client. - client := dc() + client := dcnl() resp, err := client.R(). AddRetryCondition(check). SetRetryCount(1). @@ -195,7 +195,7 @@ func TestClientRetryGet(t *testing.T) { ts := createGetServer(t) defer ts.Close() - c := dc(). + c := dcnl(). SetTimeout(time.Second * 3). SetRetryCount(3) @@ -224,7 +224,7 @@ func TestClientRetryWait(t *testing.T) { retryWaitTime := time.Duration(3) * time.Second retryMaxWaitTime := time.Duration(9) * time.Second - c := dc(). + c := dcnl(). SetRetryCount(retryCount). SetRetryWaitTime(retryWaitTime). SetRetryMaxWaitTime(retryMaxWaitTime). @@ -267,7 +267,7 @@ func TestClientRetryWaitMaxInfinite(t *testing.T) { retryWaitTime := time.Duration(3) * time.Second retryMaxWaitTime := time.Duration(-1.0) // negative value - c := dc(). + c := dcnl(). SetRetryCount(retryCount). SetRetryWaitTime(retryWaitTime). SetRetryMaxWaitTime(retryMaxWaitTime). @@ -303,7 +303,7 @@ func TestClientRetryWaitMaxMinimum(t *testing.T) { const retryMaxWaitTime = time.Nanosecond // minimal duration value - c := dc(). + c := dcnl(). SetRetryCount(1). SetRetryMaxWaitTime(retryMaxWaitTime). AddRetryCondition(func(*Response, error) bool { return true }) @@ -328,7 +328,7 @@ func TestClientRetryWaitCallbackError(t *testing.T) { return 0, errors.New("quota exceeded") } - c := dc(). + c := dcnl(). SetRetryCount(retryCount). SetRetryWaitTime(retryWaitTime). SetRetryMaxWaitTime(retryMaxWaitTime). @@ -368,7 +368,7 @@ func TestClientRetryWaitCallback(t *testing.T) { return 5 * time.Second, nil } - c := dc(). + c := dcnl(). SetRetryCount(retryCount). SetRetryWaitTime(retryWaitTime). SetRetryMaxWaitTime(retryMaxWaitTime). @@ -416,7 +416,7 @@ func TestClientRetryWaitCallbackTooShort(t *testing.T) { return 2 * time.Second, nil // too short duration } - c := dc(). + c := dcnl(). SetRetryCount(retryCount). SetRetryWaitTime(retryWaitTime). SetRetryMaxWaitTime(retryMaxWaitTime). @@ -464,7 +464,7 @@ func TestClientRetryWaitCallbackTooLong(t *testing.T) { return 4 * time.Second, nil // too long duration } - c := dc(). + c := dcnl(). SetRetryCount(retryCount). SetRetryWaitTime(retryWaitTime). SetRetryMaxWaitTime(retryMaxWaitTime). @@ -512,7 +512,7 @@ func TestClientRetryWaitCallbackSwitchToDefault(t *testing.T) { return 0, nil // use default algorithm to determine retry-after time } - c := dc(). + c := dcnl(). EnableTrace(). SetRetryCount(retryCount). SetRetryWaitTime(retryWaitTime). @@ -564,7 +564,7 @@ func TestClientRetryCancel(t *testing.T) { retryWaitTime := time.Duration(10) * time.Second retryMaxWaitTime := time.Duration(20) * time.Second - c := dc(). + c := dcnl(). SetRetryCount(retryCount). SetRetryWaitTime(retryWaitTime). SetRetryMaxWaitTime(retryMaxWaitTime). @@ -606,7 +606,7 @@ func TestClientRetryPost(t *testing.T) { var users []map[string]any users = append(users, usersmap) - c := dc() + c := dcnl() c.SetRetryCount(3) c.AddRetryCondition(RetryConditionFunc(func(r *Response, _ error) bool { return r.StatusCode() >= http.StatusInternalServerError @@ -637,7 +637,7 @@ func TestClientRetryErrorRecover(t *testing.T) { ts := createGetServer(t) defer ts.Close() - c := dc(). + c := dcnl(). SetRetryCount(2). SetError(AuthError{}). AddRetryCondition( @@ -670,7 +670,7 @@ func TestClientRetryCount(t *testing.T) { attempt := 0 - c := dc(). + c := dcnl(). SetTimeout(time.Second * 3). SetRetryCount(1). AddRetryCondition( @@ -699,7 +699,7 @@ func TestClientErrorRetry(t *testing.T) { ts := createGetServer(t) defer ts.Close() - c := dc(). + c := dcnl(). SetTimeout(time.Second * 3). SetRetryCount(1). AddRetryAfterErrorCondition() @@ -730,7 +730,7 @@ func TestClientRetryHook(t *testing.T) { attempt++ } - c := dc(). + c := dcnl(). SetRetryCount(2). SetTimeout(time.Second * 3). AddRetryHook(retryHook) @@ -786,7 +786,7 @@ func TestResetMultipartReaderSeekStartError(t *testing.T) { bytes.NewReader([]byte("test")), } - c := dc(). + c := dcnl(). SetRetryCount(2). SetTimeout(time.Second * 3). SetRetryResetReaders(true). @@ -812,7 +812,7 @@ func TestClientResetMultipartReaders(t *testing.T) { bufReader := bytes.NewReader(buf) bufCpy := make([]byte, len(buf)) - c := dc(). + c := dcnl(). SetRetryCount(2). SetTimeout(time.Second * 3). SetRetryResetReaders(true). @@ -847,7 +847,7 @@ func TestRequestResetMultipartReaders(t *testing.T) { bufReader := bytes.NewReader(buf) bufCpy := make([]byte, len(buf)) - c := dc(). + c := dcnl(). SetTimeout(time.Second * 3). AddRetryAfterErrorCondition(). AddRetryHook( diff --git a/stream.go b/stream.go index 12dd0d8c..e47f0fb1 100644 --- a/stream.go +++ b/stream.go @@ -5,6 +5,7 @@ package resty import ( + "bytes" "encoding/json" "encoding/xml" "errors" @@ -97,3 +98,49 @@ func (l *limitReadCloser) Close() error { } return nil } + +var _ io.ReadCloser = (*readCopier)(nil) + +type readCopier struct { + s io.Reader + t *bytes.Buffer + c bool + f func(*bytes.Buffer) +} + +func (r *readCopier) Read(p []byte) (int, error) { + n, err := r.s.Read(p) + if n > 0 { + _, _ = r.t.Write(p[:n]) + } + if err == io.EOF || err == ErrReadExceedsThresholdLimit { + if !r.c { + r.f(r.t) + r.c = true + } + } + return n, err +} + +func (r *readCopier) Close() error { + if c, ok := r.s.(io.Closer); ok { + return c.Close() + } + return nil +} + +var _ io.ReadCloser = (*readNoOpCloser)(nil) + +type readNoOpCloser struct { + r *bytes.Reader +} + +func (r *readNoOpCloser) Read(p []byte) (int, error) { + n, err := r.r.Read(p) + if err == io.EOF { + r.r.Seek(0, 0) + } + return n, err +} + +func (r *readNoOpCloser) Close() error { return nil } diff --git a/util.go b/util.go index 47e6f68f..cd65faa7 100644 --- a/util.go +++ b/util.go @@ -265,26 +265,27 @@ func releaseBuffer(buf *bytes.Buffer) { } } -// requestBodyReleaser wraps requests's body and implements custom Close for it. +func wrapRequestBufferReleaser(r *Request) io.ReadCloser { + if r.bodyBuf == nil { + return r.RawRequest.Body + } + return &requestBufferReleaser{ + reqBuf: r.bodyBuf, + ReadCloser: r.RawRequest.Body, + } +} + +var _ io.ReadCloser = (*requestBufferReleaser)(nil) + +// requestBufferReleaser wraps request body and implements custom Close for it. // The Close method closes original body and releases request body back to sync.Pool. -type requestBodyReleaser struct { +type requestBufferReleaser struct { releaseOnce sync.Once reqBuf *bytes.Buffer io.ReadCloser } -func newRequestBodyReleaser(respBody io.ReadCloser, reqBuf *bytes.Buffer) io.ReadCloser { - if reqBuf == nil { - return respBody - } - - return &requestBodyReleaser{ - reqBuf: reqBuf, - ReadCloser: respBody, - } -} - -func (rr *requestBodyReleaser) Close() error { +func (rr *requestBufferReleaser) Close() error { err := rr.ReadCloser.Close() rr.releaseOnce.Do(func() { releaseBuffer(rr.reqBuf) diff --git a/util_curl.go b/util_curl.go index f8992161..92b51d0d 100644 --- a/util_curl.go +++ b/util_curl.go @@ -4,7 +4,6 @@ import ( "bytes" "io" "net/http" - "net/http/cookiejar" "net/url" "strings" @@ -12,33 +11,34 @@ import ( "github.com/go-resty/resty/v3/shellescape" ) -func buildCurlRequest(req *http.Request, httpCookiejar http.CookieJar) (curl string) { +func buildCurlRequest(req *Request) (curl string) { // 1. Generate curl raw headers - curl = "curl -X " + req.Method + " " // req.Host + req.URL.Path + "?" + req.URL.RawQuery + " " + req.Proto + " " - headers := dumpCurlHeaders(req) + headers := dumpCurlHeaders(req.RawRequest) for _, kv := range *headers { curl += `-H ` + shellescape.Quote(kv[0]+": "+kv[1]) + ` ` } // 2. Generate curl cookies // TODO validate this block of code, I think its not required since cookie captured via Headers - if cookieJar, ok := httpCookiejar.(*cookiejar.Jar); ok { - cookies := cookieJar.Cookies(req.URL) - if len(cookies) > 0 { + if cookieJar := req.client.CookieJar(); cookieJar != nil { + if cookies := cookieJar.Cookies(req.RawRequest.URL); len(cookies) > 0 { curl += ` -H ` + shellescape.Quote(dumpCurlCookies(cookies)) + " " } } // 3. Generate curl body - if req.Body != nil { - buf, _ := io.ReadAll(req.Body) - req.Body = io.NopCloser(bytes.NewBuffer(buf)) // important!! + if req.RawRequest.GetBody != nil { + body, err := req.RawRequest.GetBody() + if err != nil { + return "" + } + buf, _ := io.ReadAll(body) curl += `-d ` + shellescape.Quote(string(bytes.TrimRight(buf, "\n"))) } - urlString := shellescape.Quote(req.URL.String()) + urlString := shellescape.Quote(req.RawRequest.URL.String()) if urlString == "''" { urlString = "'http://unexecuted-request'" } diff --git a/util_test.go b/util_test.go index ccf816c8..0c245032 100644 --- a/util_test.go +++ b/util_test.go @@ -9,6 +9,7 @@ import ( "errors" "mime/multipart" "net/url" + "strings" "testing" ) @@ -108,6 +109,8 @@ func TestRestyErrorFuncs(t *testing.T) { ne1 := errors.New("new error 1") nie1 := errors.New("inner error 1") + assertNil(t, wrapErrors(nil, nil)) + e := wrapErrors(ne1, nie1) assertEqual(t, "new error 1", e.Error()) assertEqual(t, "inner error 1", errors.Unwrap(e).Error()) @@ -118,3 +121,20 @@ func TestRestyErrorFuncs(t *testing.T) { e = wrapErrors(nil, nie1) assertEqual(t, "inner error 1", e.Error()) } + +// This test methods exist for test coverage purpose +// to validate the getter and setter +func TestUtilMiscTestCoverage(t *testing.T) { + l := &limitReadCloser{r: strings.NewReader("hello test close for no io.Closer")} + assertNil(t, l.Close()) + + r := &readCopier{s: strings.NewReader("hello test close for no io.Closer")} + assertNil(t, r.Close()) + + v := struct { + ID string `json:"id"` + Message string `json:"message"` + }{} + err := decodeJSON(bytes.NewReader([]byte(`{\" \": \"some value\"}`)), &v) + assertEqual(t, "invalid character '\\\\' looking for beginning of object key string", err.Error()) +}