diff --git a/response.go b/response.go index 152a9aa..d3138b0 100644 --- a/response.go +++ b/response.go @@ -71,13 +71,28 @@ func (r Responder) Trace(fn func(...interface{})) Responder { } } -// ResponderFromResponse wraps an *http.Response in a Responder +// ResponderFromResponse wraps an *http.Response in a Responder. +// +// Be careful, except for responses generated by httpmock +// (NewStringResponse and NewBytesResponse functions) for which there +// is no problems, it is the caller responsibility to ensure the +// response body can be read several times and concurrently if needed, +// as it is shared among all Responder returned responses. +// +// For home-made responses, NewRespBodyFromString and +// NewRespBodyFromBytes functions can be used to produce response +// bodies that can be read several times and concurrently. func ResponderFromResponse(resp *http.Response) Responder { return func(req *http.Request) (*http.Response, error) { - res := new(http.Response) - *res = *resp + res := *resp + + // Our stuff: generate a new io.ReadCloser instance sharing the same buffer + if body, ok := resp.Body.(*dummyReadCloser); ok { + res.Body = body.copy() + } + res.Request = req - return res, nil + return &res, nil } } @@ -244,20 +259,39 @@ func NewXmlResponderOrPanic(status int, body interface{}) Responder { // nolint: // NewRespBodyFromString creates an io.ReadCloser from a string that is suitable for use as an // http response body. func NewRespBodyFromString(body string) io.ReadCloser { - return &dummyReadCloser{strings.NewReader(body)} + return &dummyReadCloser{orig: body} } // NewRespBodyFromBytes creates an io.ReadCloser from a byte slice that is suitable for use as an // http response body. func NewRespBodyFromBytes(body []byte) io.ReadCloser { - return &dummyReadCloser{bytes.NewReader(body)} + return &dummyReadCloser{orig: body} } type dummyReadCloser struct { - body io.ReadSeeker + orig interface{} // string or []byte + body io.ReadSeeker // instanciated on demand from orig +} + +// copy returns a new instance resetting d.body to nil. +func (d *dummyReadCloser) copy() *dummyReadCloser { + return &dummyReadCloser{orig: d.orig} +} + +// setup ensures d.body is correctly initialized. +func (d *dummyReadCloser) setup() { + if d.body == nil { + switch body := d.orig.(type) { + case string: + d.body = strings.NewReader(body) + case []byte: + d.body = bytes.NewReader(body) + } + } } func (d *dummyReadCloser) Read(p []byte) (n int, err error) { + d.setup() n, err = d.body.Read(p) if err == io.EOF { d.body.Seek(0, 0) // nolint: errcheck @@ -266,6 +300,7 @@ func (d *dummyReadCloser) Read(p []byte) (n int, err error) { } func (d *dummyReadCloser) Close() error { + d.setup() d.body.Seek(0, 0) // nolint: errcheck return nil } diff --git a/response_test.go b/response_test.go index 0f261c7..bdc5745 100644 --- a/response_test.go +++ b/response_test.go @@ -6,6 +6,8 @@ import ( "errors" "io/ioutil" "net/http" + "strings" + "sync" "testing" ) @@ -340,3 +342,36 @@ func TestResponder(t *testing.T) { chk(rt, resp, "") chkCalled() } + +func TestParallelResponder(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "http://foo.bar", nil) + if err != nil { + t.Fatal("Error creating request") + } + + body := strings.Repeat("ABC-", 1000) + + for _, r := range []Responder{ + NewStringResponder(200, body), + NewBytesResponder(200, []byte(body)), + } { + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + resp, _ := r(req) + b, err := ioutil.ReadAll(resp.Body) + switch { + case err != nil: + t.Errorf("ReadAll error: %s", err) + case len(b) != 4000: + t.Errorf("ReadAll read only %d bytes", len(b)) + case !strings.HasPrefix(string(b), "ABC-"): + t.Errorf("ReadAll does not read the right prefix: %s", string(b)[0:4]) + } + }() + } + wg.Wait() + } +}