diff --git a/middleware.go b/middleware.go index 333be41d..b614af4c 100644 --- a/middleware.go +++ b/middleware.go @@ -15,6 +15,7 @@ import ( "os" "path/filepath" "reflect" + "strconv" "strings" "time" ) @@ -134,45 +135,34 @@ func parseRequestHeader(c *Client, r *Request) error { return nil } -func parseRequestBody(c *Client, r *Request) (err error) { +func parseRequestBody(c *Client, r *Request) error { if isPayloadSupported(r.Method, c.AllowGetMethodPayload) { - // Handling Multipart - if r.isMultiPart { - if err = handleMultipart(c, r); err != nil { - return + switch { + case r.isMultiPart: // Handling Multipart + if err := handleMultipart(c, r); err != nil { + return err } - - goto CL - } - - // Handling Form Data - if len(c.FormData) > 0 || len(r.FormData) > 0 { + case len(c.FormData) > 0 || len(r.FormData) > 0: // Handling Form Data handleFormData(c, r) - - goto CL - } - - // Handling Request body - if r.Body != nil { + case r.Body != nil: // Handling Request body handleContentType(c, r) - if err = handleRequestBody(c, r); err != nil { - return + if err := handleRequestBody(c, r); err != nil { + return err } } } -CL: // by default resty won't set content length, you can if you want to :) if c.setContentLength || r.setContentLength { if r.bodyBuf == nil { r.Header.Set(hdrContentLengthKey, "0") } else { - r.Header.Set(hdrContentLengthKey, fmt.Sprintf("%d", r.bodyBuf.Len())) + r.Header.Set(hdrContentLengthKey, strconv.Itoa(r.bodyBuf.Len())) } } - return + return nil } func createHTTPRequest(c *Client, r *Request) (err error) { @@ -370,13 +360,13 @@ func parseResponseBody(c *Client, res *Response) (err error) { return } -func handleMultipart(c *Client, r *Request) (err error) { +func handleMultipart(c *Client, r *Request) error { r.bodyBuf = acquireBuffer() w := multipart.NewWriter(r.bodyBuf) for k, v := range c.FormData { for _, iv := range v { - if err = w.WriteField(k, iv); err != nil { + if err := w.WriteField(k, iv); err != nil { return err } } @@ -385,12 +375,11 @@ func handleMultipart(c *Client, r *Request) (err error) { for k, v := range r.FormData { for _, iv := range v { if strings.HasPrefix(k, "@") { // file - err = addFile(w, k[1:], iv) - if err != nil { - return + if err := addFile(w, k[1:], iv); err != nil { + return err } } else { // form value - if err = w.WriteField(k, iv); err != nil { + if err := w.WriteField(k, iv); err != nil { return err } } @@ -398,50 +387,33 @@ func handleMultipart(c *Client, r *Request) (err error) { } // #21 - adding io.Reader support - if len(r.multipartFiles) > 0 { - for _, f := range r.multipartFiles { - err = addFileReader(w, f) - if err != nil { - return - } + for _, f := range r.multipartFiles { + if err := addFileReader(w, f); err != nil { + return err } } // GitHub #130 adding multipart field support with content type - if len(r.multipartFields) > 0 { - for _, mf := range r.multipartFields { - if err = addMultipartFormField(w, mf); err != nil { - return - } + for _, mf := range r.multipartFields { + if err := addMultipartFormField(w, mf); err != nil { + return err } } r.Header.Set(hdrContentTypeKey, w.FormDataContentType()) - err = w.Close() - - return + return w.Close() } func handleFormData(c *Client, r *Request) { - formData := url.Values{} - for k, v := range c.FormData { - for _, iv := range v { - formData.Add(k, iv) - } - } - - for k, v := range r.FormData { - // remove form data field from client level by key - // since overrides happens for that key in the request - formData.Del(k) - - for _, iv := range v { - formData.Add(k, iv) + if _, ok := r.FormData[k]; ok { + continue } + r.FormData[k] = v[:] } - r.bodyBuf = bytes.NewBuffer([]byte(formData.Encode())) + r.bodyBuf = acquireBuffer() + r.bodyBuf.WriteString(r.FormData.Encode()) r.Header.Set(hdrContentTypeKey, formContentType) r.isFormData = true } @@ -454,45 +426,43 @@ func handleContentType(c *Client, r *Request) { } } -func handleRequestBody(c *Client, r *Request) (err error) { +func handleRequestBody(c *Client, r *Request) error { var bodyBytes []byte - contentType := r.Header.Get(hdrContentTypeKey) - kind := kindOf(r.Body) + releaseBuffer(r.bodyBuf) r.bodyBuf = nil - if reader, ok := r.Body.(io.Reader); ok { + switch body := r.Body.(type) { + case io.Reader: if c.setContentLength || r.setContentLength { // keep backward compatibility r.bodyBuf = acquireBuffer() - _, err = r.bodyBuf.ReadFrom(reader) + if _, err := r.bodyBuf.ReadFrom(body); err != nil { + return err + } r.Body = nil } else { // Otherwise buffer less processing for `io.Reader`, sounds good. - return - } - } else if b, ok := r.Body.([]byte); ok { - bodyBytes = b - } else if s, ok := r.Body.(string); ok { - bodyBytes = []byte(s) - } else if IsJSONType(contentType) && - (kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) { - r.bodyBuf, err = jsonMarshal(c, r, r.Body) - if err != nil { - return + return nil + } + case []byte: + bodyBytes = body + case string: + bodyBytes = []byte(body) + default: + contentType := r.Header.Get(hdrContentTypeKey) + kind := kindOf(r.Body) + var err error + if IsJSONType(contentType) && (kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) { + r.bodyBuf, err = jsonMarshal(c, r, r.Body) + } else if IsXMLType(contentType) && (kind == reflect.Struct) { + bodyBytes, err = c.XMLMarshal(r.Body) } - } else if IsXMLType(contentType) && (kind == reflect.Struct) { - bodyBytes, err = c.XMLMarshal(r.Body) if err != nil { - return + return err } } if bodyBytes == nil && r.bodyBuf == nil { - err = errors.New("unsupported 'Body' type/value") - } - - // if any errors during body bytes handling, return it - if err != nil { - return + return errors.New("unsupported 'Body' type/value") } // []byte into Buffer @@ -501,7 +471,7 @@ func handleRequestBody(c *Client, r *Request) (err error) { _, _ = r.bodyBuf.Write(bodyBytes) } - return + return nil } func saveResponseIntoFile(c *Client, res *Response) error { diff --git a/middleware_test.go b/middleware_test.go index a9bac8c7..6ec91f96 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -1,9 +1,16 @@ package resty import ( + "bytes" + "encoding/json" + "errors" + "io" + "mime" + "mime/multipart" "net/http" "net/url" "reflect" + "strings" "testing" ) @@ -343,3 +350,624 @@ func Benchmark_parseRequestHeader(b *testing.B) { } } } + +type errorReader struct{} + +func (errorReader) Read(p []byte) (n int, err error) { + return 0, errors.New("fake") +} + +func Test_parseRequestBody(t *testing.T) { + for _, tt := range []struct { + name string + init func(c *Client, r *Request) + expectedBodyBuf []byte + expectedContentLength string + expectedContentType string + wantErr bool + }{ + { + name: "empty body", + init: func(c *Client, r *Request) {}, + }, + { + name: "empty body with SetContentLength by request", + init: func(c *Client, r *Request) { + r.SetContentLength(true) + }, + expectedContentLength: "0", + }, + { + name: "empty body with SetContentLength by client", + init: func(c *Client, r *Request) { + c.SetContentLength(true) + }, + expectedContentLength: "0", + }, + { + name: "string body", + init: func(c *Client, r *Request) { + r.SetBody("foo") + }, + expectedBodyBuf: []byte("foo"), + expectedContentType: plainTextType, + }, + { + name: "string body with GET method", + init: func(c *Client, r *Request) { + r.SetBody("foo") + r.Method = http.MethodGet + }, + }, + { + name: "string body with GET method and AllowGetMethodPayload", + init: func(c *Client, r *Request) { + c.SetAllowGetMethodPayload(true) + r.SetBody("foo") + r.Method = http.MethodGet + }, + expectedBodyBuf: []byte("foo"), + expectedContentType: plainTextType, + }, + { + name: "string body with HEAD method", + init: func(c *Client, r *Request) { + r.SetBody("foo") + r.Method = http.MethodHead + }, + }, + { + name: "string body with OPTIONS method", + init: func(c *Client, r *Request) { + r.SetBody("foo") + r.Method = http.MethodOptions + }, + }, + { + name: "string body with POST method", + init: func(c *Client, r *Request) { + r.SetBody("foo") + r.Method = http.MethodPost + }, + expectedBodyBuf: []byte("foo"), + expectedContentType: plainTextType, + }, + { + name: "string body with PATCH method", + init: func(c *Client, r *Request) { + r.SetBody("foo") + r.Method = http.MethodPatch + }, + expectedBodyBuf: []byte("foo"), + expectedContentType: plainTextType, + }, + { + name: "string body with PUT method", + init: func(c *Client, r *Request) { + r.SetBody("foo") + r.Method = http.MethodPut + }, + expectedBodyBuf: []byte("foo"), + expectedContentType: plainTextType, + }, + { + name: "string body with DELETE method", + init: func(c *Client, r *Request) { + r.SetBody("foo") + r.Method = http.MethodDelete + }, + expectedBodyBuf: []byte("foo"), + expectedContentType: plainTextType, + }, + { + name: "string body with CONNECT method", + init: func(c *Client, r *Request) { + r.SetBody("foo") + r.Method = http.MethodConnect + }, + expectedBodyBuf: []byte("foo"), + expectedContentType: plainTextType, + }, + { + name: "string body with TRACE method", + init: func(c *Client, r *Request) { + r.SetBody("foo") + r.Method = http.MethodTrace + }, + expectedBodyBuf: []byte("foo"), + expectedContentType: plainTextType, + }, + { + name: "string body with BAR method", + init: func(c *Client, r *Request) { + r.SetBody("foo") + r.Method = "BAR" + }, + expectedBodyBuf: []byte("foo"), + expectedContentType: plainTextType, + }, + { + name: "byte body", + init: func(c *Client, r *Request) { + r.SetBody([]byte("foo")) + }, + expectedBodyBuf: []byte("foo"), + expectedContentType: plainTextType, + }, + { + name: "io.Reader body, no bodyBuf", + init: func(c *Client, r *Request) { + r.SetBody(bytes.NewBufferString("foo")) + }, + expectedContentType: jsonContentType, + }, + { + name: "io.Reader body with SetContentLength by request", + init: func(c *Client, r *Request) { + r.SetBody(bytes.NewBufferString("foo")). + SetContentLength(true) + }, + expectedBodyBuf: []byte("foo"), + expectedContentLength: "3", + expectedContentType: jsonContentType, + }, + { + name: "io.Reader body with SetContentLength by client", + init: func(c *Client, r *Request) { + c.SetContentLength(true) + r.SetBody(bytes.NewBufferString("foo")) + }, + expectedBodyBuf: []byte("foo"), + expectedContentLength: "3", + expectedContentType: jsonContentType, + }, + { + name: "form data by request", + init: func(c *Client, r *Request) { + r.SetFormData(map[string]string{ + "foo": "1", + "bar": "2", + }) + }, + expectedBodyBuf: []byte("foo=1&bar=2"), + expectedContentType: formContentType, + }, + { + name: "form data by client", + init: func(c *Client, r *Request) { + c.SetFormData(map[string]string{ + "foo": "1", + "bar": "2", + }) + }, + expectedBodyBuf: []byte("foo=1&bar=2"), + expectedContentType: formContentType, + }, + { + name: "form data by client and request", + init: func(c *Client, r *Request) { + c.SetFormData(map[string]string{ + "foo": "1", + "bar": "2", + }) + r.SetFormData(map[string]string{ + "foo": "3", + "baz": "4", + }) + }, + expectedBodyBuf: []byte("foo=3&bar=2&baz=4"), + expectedContentType: formContentType, + }, + { + name: "json from struct", + init: func(c *Client, r *Request) { + r.SetBody(struct { + Foo string `json:"foo"` + Bar string `json:"bar"` + }{ + Foo: "1", + Bar: "2", + }).SetContentLength(true) + }, + expectedBodyBuf: []byte(`{"foo":"1","bar":"2"}`), + expectedContentType: jsonContentType, + expectedContentLength: "21", + }, + { + name: "json from slice", + init: func(c *Client, r *Request) { + r.SetBody([]string{"foo", "bar"}).SetContentLength(true) + }, + expectedBodyBuf: []byte(`["foo","bar"]`), + expectedContentType: jsonContentType, + expectedContentLength: "13", + }, + { + name: "json from map", + init: func(c *Client, r *Request) { + r.SetBody(map[string]interface{}{ + "foo": "1", + "bar": []int{1, 2, 3}, + "baz": map[string]string{ + "qux": "4", + }, + "xyz": nil, + }).SetContentLength(true) + }, + expectedBodyBuf: []byte(`{"bar":[1,2,3],"baz":{"qux":"4"},"foo":"1","xyz":null}`), + expectedContentType: jsonContentType, + expectedContentLength: "54", + }, + { + name: "json from map", + init: func(c *Client, r *Request) { + r.SetBody(map[string]interface{}{ + "foo": "1", + "bar": []int{1, 2, 3}, + "baz": map[string]string{ + "qux": "4", + }, + "xyz": nil, + }).SetContentLength(true) + }, + expectedBodyBuf: []byte(`{"bar":[1,2,3],"baz":{"qux":"4"},"foo":"1","xyz":null}`), + expectedContentType: jsonContentType, + expectedContentLength: "54", + }, + { + name: "json from map", + init: func(c *Client, r *Request) { + r.SetBody(map[string]interface{}{ + "foo": "1", + "bar": []int{1, 2, 3}, + "baz": map[string]string{ + "qux": "4", + }, + "xyz": nil, + }).SetContentLength(true) + }, + expectedBodyBuf: []byte(`{"bar":[1,2,3],"baz":{"qux":"4"},"foo":"1","xyz":null}`), + expectedContentType: jsonContentType, + expectedContentLength: "54", + }, + { + name: "xml from struct", + init: func(c *Client, r *Request) { + type FooBar struct { + Foo string `xml:"foo"` + Bar string `xml:"bar"` + } + r.SetBody(FooBar{ + Foo: "1", + Bar: "2", + }). + SetContentLength(true). + SetHeader(hdrContentTypeKey, "text/xml") + }, + expectedBodyBuf: []byte(`12`), + expectedContentType: "text/xml", + expectedContentLength: "41", + }, + { + name: "mulipart form data", + init: func(c *Client, r *Request) { + c.SetFormData(map[string]string{ + "foo": "1", + "bar": "2", + }) + r.SetFormData(map[string]string{ + "foo": "3", + "baz": "4", + }) + r.SetMultipartFormData(map[string]string{ + "foo": "5", + "xyz": "6", + }).SetContentLength(true) + }, + expectedBodyBuf: []byte(`{"bar":"2", "baz":"4", "foo":"5", "xyz":"6"}`), + expectedContentType: "multipart/form-data; boundary=", + expectedContentLength: "744", + }, + { + name: "mulipart fields", + init: func(c *Client, r *Request) { + r.SetMultipartFields( + &MultipartField{ + Param: "foo", + ContentType: "text/plain", + Reader: strings.NewReader("1"), + }, + &MultipartField{ + Param: "bar", + ContentType: "text/plain", + Reader: strings.NewReader("2"), + }, + ).SetContentLength(true) + }, + expectedBodyBuf: []byte(`{"bar":"2","foo":"1"}`), + expectedContentType: "multipart/form-data; boundary=", + expectedContentLength: "344", + }, + { + name: "mulipart files", + init: func(c *Client, r *Request) { + r.SetFileReader("foo", "foo.txt", strings.NewReader("1")). + SetFileReader("bar", "bar.txt", strings.NewReader("2")). + SetContentLength(true) + }, + expectedBodyBuf: []byte(`{"bar":"2","foo":"1"}`), + expectedContentType: "multipart/form-data; boundary=", + expectedContentLength: "412", + }, + { + name: "body with errorReader", + init: func(c *Client, r *Request) { + r.SetBody(&errorReader{}).SetContentLength(true) + }, + wantErr: true, + }, + { + name: "unsupported type", + init: func(c *Client, r *Request) { + r.SetBody(1) + }, + wantErr: true, + }, + { + name: "unsupported xml", + init: func(c *Client, r *Request) { + r.SetBody(struct { + Foo string `xml:"foo"` + Bar string `xml:"bar"` + }{ + Foo: "1", + Bar: "2", + }).Header.Set(hdrContentTypeKey, "text/xml") + }, + wantErr: true, + }, + { + name: "multipart fields with errorReader", + init: func(c *Client, r *Request) { + r.SetMultipartFields(&MultipartField{ + Param: "foo", + ContentType: "text/plain", + Reader: &errorReader{}, + }) + }, + wantErr: true, + }, + { + name: "multipart files with errorReader", + init: func(c *Client, r *Request) { + r.SetFileReader("foo", "foo.txt", &errorReader{}) + }, + wantErr: true, + }, + { + name: "multipart with file not found", + init: func(c *Client, r *Request) { + r.SetFormData(map[string]string{ + "@foo": "foo.txt", + }) + r.isMultiPart = true + }, + wantErr: true, + }, + } { + t.Run(tt.name, func(t *testing.T) { + c := New() + r := c.R() + tt.init(c, r) + if err := parseRequestBody(c, r); err != nil { + if tt.wantErr { + return + } + t.Errorf("parseRequestBody() error = %v", err) + } else if tt.wantErr { + t.Errorf("wanted error, but got nil") + } + switch { + case r.bodyBuf == nil && tt.expectedBodyBuf != nil: + t.Errorf("bodyBuf is nil, but expected: %s", string(tt.expectedBodyBuf)) + case r.bodyBuf != nil && tt.expectedBodyBuf == nil: + t.Errorf("bodyBuf is not nil, but expected nil: %s", r.bodyBuf.String()) + case r.bodyBuf != nil && tt.expectedBodyBuf != nil: + var actual, expected interface{} = r.bodyBuf.Bytes(), tt.expectedBodyBuf + if r.isFormData { + var err error + actual, err = url.ParseQuery(r.bodyBuf.String()) + if err != nil { + t.Errorf("ParseQuery(r.bodyBuf) error = %v", err) + } + expected, err = url.ParseQuery(string(tt.expectedBodyBuf)) + if err != nil { + t.Errorf("ParseQuery(tt.expectedBodyBuf) error = %v", err) + } + } else if r.isMultiPart { + _, params, err := mime.ParseMediaType(r.Header.Get(hdrContentTypeKey)) + if err != nil { + t.Errorf("ParseMediaType(hdrContentTypeKey) error = %v", err) + } + boundary, ok := params["boundary"] + if !ok { + t.Errorf("boundary not found in Content-Type header") + } + reader := multipart.NewReader(r.bodyBuf, boundary) + body := make(map[string]interface{}) + for part, perr := reader.NextPart(); perr != io.EOF; part, perr = reader.NextPart() { + if perr != nil { + t.Errorf("NextPart() error = %v", perr) + } + name := part.FormName() + if name == "" { + name = part.FileName() + } + data, err := io.ReadAll(part) + if err != nil { + t.Errorf("ReadAll(part) error = %v", err) + } + body[name] = string(data) + } + actual = body + expected = nil + if err := json.Unmarshal(tt.expectedBodyBuf, &expected); err != nil { + t.Errorf("json.Unmarshal(tt.expectedBodyBuf) error = %v", err) + } + t.Logf(`in case of an error, the expected body should be set as json for object: %#+v`, actual) + } + if !reflect.DeepEqual(actual, expected) { + t.Errorf("bodyBuf = %q does not match expected %q", r.bodyBuf.String(), string(tt.expectedBodyBuf)) + } + } + if tt.expectedContentLength != r.Header.Get(hdrContentLengthKey) { + t.Errorf("Content-Length header = %q does not match expected %q", r.Header.Get(hdrContentLengthKey), tt.expectedContentLength) + } + if ct := r.Header.Get(hdrContentTypeKey); !((tt.expectedContentType == "" && ct != "") || strings.Contains(ct, tt.expectedContentType)) { + t.Errorf("Content-Type header = %q does not match expected %q", r.Header.Get(hdrContentTypeKey), tt.expectedContentType) + } + }) + } +} + +func Benchmark_parseRequestBody_string(b *testing.B) { + c := New() + r := c.R() + r.SetBody("foo").SetContentLength(true) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := parseRequestBody(c, r); err != nil { + b.Errorf("parseRequestBody() error = %v", err) + } + } +} + +func Benchmark_parseRequestBody_byte(b *testing.B) { + c := New() + r := c.R() + r.SetBody([]byte("foo")).SetContentLength(true) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := parseRequestBody(c, r); err != nil { + b.Errorf("parseRequestBody() error = %v", err) + } + } +} + +func Benchmark_parseRequestBody_reader_with_SetContentLength(b *testing.B) { + c := New() + r := c.R() + r.SetBody(bytes.NewBufferString("foo")).SetContentLength(true) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := parseRequestBody(c, r); err != nil { + b.Errorf("parseRequestBody() error = %v", err) + } + } +} +func Benchmark_parseRequestBody_reader_without_SetContentLength(b *testing.B) { + c := New() + r := c.R() + r.SetBody(bytes.NewBufferString("foo")) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := parseRequestBody(c, r); err != nil { + b.Errorf("parseRequestBody() error = %v", err) + } + } +} + +func Benchmark_parseRequestBody_struct(b *testing.B) { + type FooBar struct { + Foo string `json:"foo"` + Bar string `json:"bar"` + } + c := New() + r := c.R() + r.SetBody(FooBar{Foo: "1", Bar: "2"}).SetContentLength(true).SetHeader(hdrContentTypeKey, jsonContentType) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := parseRequestBody(c, r); err != nil { + b.Errorf("parseRequestBody() error = %v", err) + } + } +} + +func Benchmark_parseRequestBody_struct_xml(b *testing.B) { + type FooBar struct { + Foo string `xml:"foo"` + Bar string `xml:"bar"` + } + c := New() + r := c.R() + r.SetBody(FooBar{Foo: "1", Bar: "2"}).SetContentLength(true).SetHeader(hdrContentTypeKey, "text/xml") + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := parseRequestBody(c, r); err != nil { + b.Errorf("parseRequestBody() error = %v", err) + } + } +} + +func Benchmark_parseRequestBody_map(b *testing.B) { + c := New() + r := c.R() + r.SetBody(map[string]string{ + "foo": "1", + "bar": "2", + }).SetContentLength(true).SetHeader(hdrContentTypeKey, jsonContentType) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := parseRequestBody(c, r); err != nil { + b.Errorf("parseRequestBody() error = %v", err) + } + } +} + +func Benchmark_parseRequestBody_slice(b *testing.B) { + c := New() + r := c.R() + r.SetBody([]string{"1", "2"}).SetContentLength(true).SetHeader(hdrContentTypeKey, jsonContentType) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := parseRequestBody(c, r); err != nil { + b.Errorf("parseRequestBody() error = %v", err) + } + } +} + +func Benchmark_parseRequestBody_FormData(b *testing.B) { + c := New() + r := c.R() + c.SetFormData(map[string]string{"foo": "1", "bar": "2"}) + r.SetFormData(map[string]string{"foo": "3", "baz": "4"}).SetContentLength(true) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := parseRequestBody(c, r); err != nil { + b.Errorf("parseRequestBody() error = %v", err) + } + } +} + +func Benchmark_parseRequestBody_MultiPart(b *testing.B) { + c := New() + r := c.R() + c.SetFormData(map[string]string{"foo": "1", "bar": "2"}) + r.SetFormData(map[string]string{"foo": "3", "baz": "4"}). + SetMultipartFormData(map[string]string{"foo": "5", "xyz": "6"}). + SetFileReader("qwe", "qwe.txt", strings.NewReader("7")). + SetMultipartFields( + &MultipartField{ + Param: "sdj", + ContentType: "text/plain", + Reader: strings.NewReader("8"), + }, + ). + SetContentLength(true) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := parseRequestBody(c, r); err != nil { + b.Errorf("parseRequestBody() error = %v", err) + } + } +}