Repository: gavv/httpexpect (go)

Issue:
Lazy reading of response body
Currently, response body is read entirely and closed inside Response constructor. This prevents Response to work with stream responses, e.g. SSE.

To fix this, Response should not read entire body in constructor, but instead read it only on demand when it's needed first time, e.g. when the user invoked Text() or JSON(). This way it'll continue to work as before for regular responses and wont break with stream responses, given that the user does not try to invoke inappropriate methods on them.

We need to find all places when `r.content` is used in response.go and replace it with a function call which will check if response body is already read, and read it if necessary.

We should add unit test for Response that checks that body is not read until a call to Text/JSON/etc.

Pull request:
Add lazy reading of response body
Fixes #244 

Patch:
diff --git a/response.go b/response.go
index 9f9d1b9..59f495d 100644
--- a/response.go
+++ b/response.go
@@ -28,10 +28,20 @@ type Response struct {
 	websocket *websocket.Conn
 	rtt       *time.Duration
 
-	content []byte
+	content      []byte
+	contentState contentState
+
 	cookies []*http.Cookie
 }
 
+type contentState int
+
+const (
+	contentPending contentState = iota
+	contentRetreived
+	contentFailed
+)
+
 // NewResponse returns a new Response instance.
 //
 // If reporter is nil, the function panics.
@@ -85,53 +95,74 @@ func newResponse(opts responseOpts) *Response {
 	opts.config.validate()
 
 	r := &Response{
-		config: opts.config,
-		chain:  opts.chain.clone(),
+		config:       opts.config,
+		chain:        opts.chain.clone(),
+		contentState: contentPending,
 	}
 
 	opChain := r.chain.enter("")
 	defer opChain.leave()
 
-	if opts.httpResp == nil {
+	if len(opts.rtt) > 1 {
 		opChain.fail(AssertionFailure{
-			Type:   AssertNotNil,
-			Actual: &AssertionValue{opts.httpResp},
+			Type: AssertUsage,
 			Errors: []error{
-				errors.New("expected: non-nil response"),
+				errors.New("unexpected multiple rtt arguments"),
 			},
 		})
 		return r
 	}
 
-	if len(opts.rtt) > 1 {
+	if len(opts.rtt) > 0 {
+		rttCopy := opts.rtt[0]
+		r.rtt = &rttCopy
+	}
+
+	if opts.httpResp == nil {
 		opChain.fail(AssertionFailure{
-			Type: AssertUsage,
+			Type:   AssertNotNil,
+			Actual: &AssertionValue{opts.httpResp},
 			Errors: []error{
-				errors.New("unexpected multiple rtt arguments"),
+				errors.New("expected: non-nil response"),
 			},
 		})
 		return r
 	}
 
 	r.httpResp = opts.httpResp
-	r.websocket = opts.websocket
-
-	r.content = getResponseContent(opChain, r.httpResp)
-	r.cookies = r.httpResp.Cookies()
 
-	if len(opts.rtt) > 0 {
-		rtt := opts.rtt[0]
-		r.rtt = &rtt
+	if r.httpResp.Body != nil && r.httpResp.Body != http.NoBody {
+		if _, ok := r.httpResp.Body.(*bodyWrapper); !ok {
+			respCopy := *r.httpResp
+			r.httpResp = &respCopy
+			r.httpResp.Body = newBodyWrapper(r.httpResp.Body, nil)
+		}
 	}
 
+	r.websocket = opts.websocket
+	r.cookies = r.httpResp.Cookies()
+
 	r.chain.setResponse(r)
 
 	return r
 }
 
-func getResponseContent(opChain *chain, resp *http.Response) []byte {
-	if resp.Body == nil {
-		return []byte{}
+func (r *Response) getContent(opChain *chain) ([]byte, bool) {
+	switch r.contentState {
+	case contentRetreived:
+		return r.content, true
+
+	case contentFailed:
+		return nil, false
+
+	case contentPending:
+		break
+	}
+
+	resp := r.httpResp
+
+	if resp.Body == nil || resp.Body == http.NoBody {
+		return []byte{}, true
 	}
 
 	if bw, ok := resp.Body.(*bodyWrapper); ok {
@@ -153,10 +184,17 @@ func getResponseContent(opChain *chain, resp *http.Response) []byte {
 				err,
 			},
 		})
-		return nil
+
+		r.content = nil
+		r.contentState = contentFailed
+
+		return nil, false
 	}
 
-	return content
+	r.content = content
+	r.contentState = contentRetreived
+
+	return r.content, true
 }
 
 // Raw returns underlying http.Response object.
@@ -528,7 +566,16 @@ func (r *Response) Body() *String {
 	opChain := r.chain.enter("Body()")
 	defer opChain.leave()
 
-	return newString(opChain, string(r.content))
+	if opChain.failed() {
+		return newString(opChain, "")
+	}
+
+	content, ok := r.getContent(opChain)
+	if !ok {
+		return newString(opChain, "")
+	}
+
+	return newString(opChain, string(content))
 }
 
 // NoContent succeeds if response contains empty Content-Type header and
@@ -542,9 +589,17 @@ func (r *Response) NoContent() *Response {
 	}
 
 	contentType := r.httpResp.Header.Get("Content-Type")
+	if !r.checkEqual(opChain, `"Content-Type" header`, "", contentType) {
+		return r
+	}
 
-	r.checkEqual(opChain, `"Content-Type" header`, "", contentType)
-	r.checkEqual(opChain, "body", "", string(r.content))
+	content, ok := r.getContent(opChain)
+	if !ok {
+		return r
+	}
+	if !r.checkEqual(opChain, "body", "", string(content)) {
+		return r
+	}
 
 	return r
 }
@@ -656,9 +711,12 @@ func (r *Response) Text(options ...ContentOpts) *String {
 		return newString(opChain, "")
 	}
 
-	content := string(r.content)
+	content, ok := r.getContent(opChain)
+	if !ok {
+		return newString(opChain, "")
+	}
 
-	return newString(opChain, content)
+	return newString(opChain, string(content))
 }
 
 // Form returns a new Object instance with form decoded from response body.
@@ -704,7 +762,12 @@ func (r *Response) getForm(
 		return nil
 	}
 
-	decoder := form.NewDecoder(bytes.NewReader(r.content))
+	content, ok := r.getContent(opChain)
+	if !ok {
+		return nil
+	}
+
+	decoder := form.NewDecoder(bytes.NewReader(content))
 
 	var object map[string]interface{}
 
@@ -712,7 +775,7 @@ func (r *Response) getForm(
 		opChain.fail(AssertionFailure{
 			Type: AssertValid,
 			Actual: &AssertionValue{
-				string(r.content),
+				string(content),
 			},
 			Errors: []error{
 				errors.New("failed to decode form"),
@@ -765,13 +828,18 @@ func (r *Response) getJSON(opChain *chain, options ...ContentOpts) interface{} {
 		return nil
 	}
 
+	content, ok := r.getContent(opChain)
+	if !ok {
+		return nil
+	}
+
 	var value interface{}
 
-	if err := json.Unmarshal(r.content, &value); err != nil {
+	if err := json.Unmarshal(content, &value); err != nil {
 		opChain.fail(AssertionFailure{
 			Type: AssertValid,
 			Actual: &AssertionValue{
-				string(r.content),
+				string(content),
 			},
 			Errors: []error{
 				errors.New("failed to decode json"),
@@ -784,7 +852,7 @@ func (r *Response) getJSON(opChain *chain, options ...ContentOpts) interface{} {
 	return value
 }
 
-// JSON returns a new Value instance with JSONP decoded from response body.
+// JSONP returns a new Value instance with JSONP decoded from response body.
 //
 // JSONP succeeds if response contains "application/javascript" Content-Type
 // header with empty or "utf-8" charset and response body of the following form:
@@ -838,13 +906,18 @@ func (r *Response) getJSONP(
 		return nil
 	}
 
-	m := jsonp.FindSubmatch(r.content)
+	content, ok := r.getContent(opChain)
+	if !ok {
+		return nil
+	}
+
+	m := jsonp.FindSubmatch(content)
 
 	if len(m) != 3 || string(m[1]) != callback {
 		opChain.fail(AssertionFailure{
 			Type: AssertValid,
 			Actual: &AssertionValue{
-				string(r.content),
+				string(content),
 			},
 			Errors: []error{
 				fmt.Errorf(`expected: JSONP body in form of "%s(<valid json>)"`,
@@ -860,7 +933,7 @@ func (r *Response) getJSONP(
 		opChain.fail(AssertionFailure{
 			Type: AssertValid,
 			Actual: &AssertionValue{
-				string(r.content),
+				string(content),
 			},
 			Errors: []error{
 				errors.New("failed to decode json"),
@@ -954,7 +1027,9 @@ func (r *Response) checkContentType(
 	return true
 }
 
-func (r *Response) checkEqual(opChain *chain, what string, expected, actual interface{}) {
+func (r *Response) checkEqual(
+	opChain *chain, what string, expected, actual interface{},
+) bool {
 	if !reflect.DeepEqual(expected, actual) {
 		opChain.fail(AssertionFailure{
 			Type:     AssertEqual,
@@ -964,5 +1039,8 @@ func (r *Response) checkEqual(opChain *chain, what string, expected, actual inte
 				fmt.Errorf("unexpected %s value", what),
 			},
 		})
+		return false
 	}
+
+	return true
 }


Test patch:
diff --git a/body_wrapper_test.go b/body_wrapper_test.go
index 99749a4..cc4af35 100644
--- a/body_wrapper_test.go
+++ b/body_wrapper_test.go
@@ -11,9 +11,9 @@ import (
 func TestBodyWrapper_Rewind(t *testing.T) {
 	body := newMockBody("test_body")
 
-	cancelled := false
+	cancelCount := 0
 	cancelFn := func() {
-		cancelled = true
+		cancelCount++
 	}
 
 	wrp := newBodyWrapper(body, cancelFn)
@@ -22,8 +22,10 @@ func TestBodyWrapper_Rewind(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Equal(t, "test_body", string(b))
 
-	assert.True(t, body.closed)
-	assert.True(t, cancelled)
+	readCount := body.readCount
+	assert.NotEqual(t, 0, body.readCount)
+	assert.Equal(t, 1, body.closeCount)
+	assert.Equal(t, 1, cancelCount)
 
 	err = wrp.Close()
 	assert.NoError(t, err)
@@ -33,6 +35,10 @@ func TestBodyWrapper_Rewind(t *testing.T) {
 	b, err = ioutil.ReadAll(wrp)
 	assert.NoError(t, err)
 	assert.Equal(t, "test_body", string(b))
+
+	assert.Equal(t, readCount, body.readCount)
+	assert.Equal(t, 1, body.closeCount)
+	assert.Equal(t, 1, cancelCount)
 }
 
 func TestBodyWrapper_GetBody(t *testing.T) {
@@ -53,14 +59,17 @@ func TestBodyWrapper_GetBody(t *testing.T) {
 	b, err = ioutil.ReadAll(rd2)
 	assert.NoError(t, err)
 	assert.Equal(t, "test_body", string(b))
+
+	assert.NotEqual(t, 0, body.readCount)
+	assert.Equal(t, 1, body.closeCount)
 }
 
 func TestBodyWrapper_Close(t *testing.T) {
 	body := newMockBody("test_body")
 
-	cancelled := false
+	cancelCount := 0
 	cancelFn := func() {
-		cancelled = true
+		cancelCount++
 	}
 
 	wrp := newBodyWrapper(body, cancelFn)
@@ -68,8 +77,9 @@ func TestBodyWrapper_Close(t *testing.T) {
 	err := wrp.Close()
 	assert.NoError(t, err)
 
-	assert.True(t, body.closed)
-	assert.True(t, cancelled)
+	assert.NotEqual(t, 0, body.readCount)
+	assert.Equal(t, 1, body.closeCount)
+	assert.Equal(t, 1, cancelCount)
 }
 
 func TestBodyWrapper_OneError(t *testing.T) {
diff --git a/mocks_test.go b/mocks_test.go
index 2b32e65..ea456e0 100644
--- a/mocks_test.go
+++ b/mocks_test.go
@@ -125,10 +125,13 @@ func (mt *mockTransportRedirect) WithMaxRedirect(
 }
 
 type mockBody struct {
-	reader   io.Reader
-	closed   bool
-	readErr  error
-	closeErr error
+	reader io.Reader
+
+	readCount int
+	readErr   error
+
+	closeCount int
+	closeErr   error
 }
 
 func newMockBody(body string) *mockBody {
@@ -138,6 +141,7 @@ func newMockBody(body string) *mockBody {
 }
 
 func (b *mockBody) Read(p []byte) (int, error) {
+	b.readCount++
 	if b.readErr != nil {
 		return 0, b.readErr
 	}
@@ -145,7 +149,7 @@ func (b *mockBody) Read(p []byte) (int, error) {
 }
 
 func (b *mockBody) Close() error {
-	b.closed = true
+	b.closeCount++
 	if b.closeErr != nil {
 		return b.closeErr
 	}
diff --git a/request_test.go b/request_test.go
index d940a02..e56a52b 100644
--- a/request_test.go
+++ b/request_test.go
@@ -839,7 +839,7 @@ func TestRequest_BodyChunked(t *testing.T) {
 	assert.Equal(t, "METHOD", client.req.Method)
 	assert.Equal(t, "url", client.req.URL.String())
 	assert.Equal(t, make(http.Header), client.req.Header)
-	assert.Equal(t, "body", string(resp.content))
+	assert.Equal(t, "body", resp.Body().Raw())
 
 	assert.Same(t, &client.resp, resp.Raw())
 }
@@ -927,7 +927,7 @@ func TestRequest_BodyBytes(t *testing.T) {
 	assert.Equal(t, "METHOD", client.req.Method)
 	assert.Equal(t, "/path", client.req.URL.String())
 	assert.Equal(t, make(http.Header), client.req.Header)
-	assert.Equal(t, "body", string(resp.content))
+	assert.Equal(t, "body", resp.Body().Raw())
 
 	assert.Same(t, &client.resp, resp.Raw())
 }
@@ -988,7 +988,7 @@ func TestRequest_BodyText(t *testing.T) {
 	assert.Equal(t, "METHOD", client.req.Method)
 	assert.Equal(t, "url", client.req.URL.String())
 	assert.Equal(t, http.Header(expectedHeaders), client.req.Header)
-	assert.Equal(t, "some text", string(resp.content))
+	assert.Equal(t, "some text", resp.Body().Raw())
 
 	assert.Same(t, &client.resp, resp.Raw())
 }
@@ -1028,7 +1028,7 @@ func TestRequest_BodyForm(t *testing.T) {
 	assert.Equal(t, "METHOD", client.req.Method)
 	assert.Equal(t, "url", client.req.URL.String())
 	assert.Equal(t, http.Header(expectedHeaders), client.req.Header)
-	assert.Equal(t, `a=1&b=2`, string(resp.content))
+	assert.Equal(t, `a=1&b=2`, resp.Body().Raw())
 
 	assert.Same(t, &client.resp, resp.Raw())
 }
@@ -1066,7 +1066,7 @@ func TestRequest_BodyFormField(t *testing.T) {
 	assert.Equal(t, "METHOD", client.req.Method)
 	assert.Equal(t, "url", client.req.URL.String())
 	assert.Equal(t, http.Header(expectedHeaders), client.req.Header)
-	assert.Equal(t, `a=1&b=2`, string(resp.content))
+	assert.Equal(t, `a=1&b=2`, resp.Body().Raw())
 
 	assert.Same(t, &client.resp, resp.Raw())
 }
@@ -1104,7 +1104,7 @@ func TestRequest_BodyFormStruct(t *testing.T) {
 	assert.Equal(t, "METHOD", client.req.Method)
 	assert.Equal(t, "url", client.req.URL.String())
 	assert.Equal(t, http.Header(expectedHeaders), client.req.Header)
-	assert.Equal(t, `a=1&b=2`, string(resp.content))
+	assert.Equal(t, `a=1&b=2`, resp.Body().Raw())
 
 	assert.Same(t, &client.resp, resp.Raw())
 }
@@ -1142,7 +1142,7 @@ func TestRequest_BodyFormCombined(t *testing.T) {
 	assert.Equal(t, "METHOD", client.req.Method)
 	assert.Equal(t, "url", client.req.URL.String())
 	assert.Equal(t, http.Header(expectedHeaders), client.req.Header)
-	assert.Equal(t, `a=1&b=2&c=3`, string(resp.content))
+	assert.Equal(t, `a=1&b=2&c=3`, resp.Body().Raw())
 
 	assert.Same(t, &client.resp, resp.Raw())
 }
@@ -1178,7 +1178,8 @@ func TestRequest_BodyMultipart(t *testing.T) {
 	assert.Equal(t, "multipart/form-data", mediatype)
 	assert.True(t, params["boundary"] != "")
 
-	reader := multipart.NewReader(bytes.NewReader(resp.content), params["boundary"])
+	reader := multipart.NewReader(strings.NewReader(resp.Body().Raw()),
+		params["boundary"])
 
 	part1, _ := reader.NextPart()
 	assert.Equal(t, "b", part1.FormName())
@@ -1241,7 +1242,8 @@ func TestRequest_BodyMultipartFile(t *testing.T) {
 	assert.Equal(t, "multipart/form-data", mediatype)
 	assert.True(t, params["boundary"] != "")
 
-	reader := multipart.NewReader(bytes.NewReader(resp.content), params["boundary"])
+	reader := multipart.NewReader(strings.NewReader(resp.Body().Raw()),
+		params["boundary"])
 
 	part1, _ := reader.NextPart()
 	assert.Equal(t, "a", part1.FormName())
@@ -1303,7 +1305,7 @@ func TestRequest_BodyJSON(t *testing.T) {
 	assert.Equal(t, "METHOD", client.req.Method)
 	assert.Equal(t, "url", client.req.URL.String())
 	assert.Equal(t, http.Header(expectedHeaders), client.req.Header)
-	assert.Equal(t, `{"key":"value"}`, string(resp.content))
+	assert.Equal(t, `{"key":"value"}`, resp.Body().Raw())
 
 	assert.Same(t, &client.resp, resp.Raw())
 }
diff --git a/response_test.go b/response_test.go
index c19c4d1..d0fd992 100644
--- a/response_test.go
+++ b/response_test.go
@@ -116,6 +116,129 @@ func TestResponse_Constructors(t *testing.T) {
 	})
 }
 
+func TestResponse_ResponseBodyLazyRead(t *testing.T) {
+	t.Run("constructor does not read content", func(t *testing.T) {
+		reporter := newMockReporter(t)
+
+		body := newMockBody("body string")
+		resp := NewResponse(reporter, &http.Response{
+			StatusCode: http.StatusOK,
+			Body:       body,
+		})
+
+		assert.Equal(t, 0, body.readCount)
+		assert.Equal(t, 0, body.closeCount)
+		assert.Nil(t, resp.content)
+		assert.Equal(t, contentPending, resp.contentState)
+	})
+
+	t.Run("content is remembered", func(t *testing.T) {
+		reporter := newMockReporter(t)
+
+		body := newMockBody("body string")
+		resp := NewResponse(reporter, &http.Response{
+			StatusCode: http.StatusOK,
+			Body:       body,
+		})
+
+		assert.Equal(t, 0, body.readCount)
+		assert.Equal(t, 0, body.closeCount)
+		assert.Nil(t, resp.content)
+		assert.Equal(t, contentPending, resp.contentState)
+
+		// Read body
+		resp.Body()
+		resp.chain.assertNotFailed(t)
+
+		readCount := body.readCount
+		assert.NotEqual(t, 0, body.readCount)
+		assert.Equal(t, 1, body.closeCount)
+		assert.Equal(t, []byte("body string"), resp.content)
+		assert.Equal(t, contentRetreived, resp.contentState)
+
+		// Second call should be no-op
+		resp.Body()
+		resp.chain.assertNotFailed(t)
+
+		assert.Equal(t, readCount, body.readCount)
+		assert.Equal(t, 1, body.closeCount)
+		assert.Equal(t, []byte("body string"), resp.content)
+		assert.Equal(t, contentRetreived, resp.contentState)
+	})
+
+	t.Run("read error is remembered", func(t *testing.T) {
+		reporter := newMockReporter(t)
+
+		body := newMockBody("body string")
+		body.readErr = errors.New("test error")
+
+		resp := NewResponse(reporter, &http.Response{
+			StatusCode: http.StatusOK,
+			Body:       body,
+		})
+
+		assert.Equal(t, 0, body.readCount)
+		assert.Equal(t, 0, body.closeCount)
+		assert.Nil(t, resp.content)
+		assert.Equal(t, contentPending, resp.contentState)
+
+		// Read body
+		resp.Body()
+		resp.chain.assertFailed(t)
+
+		readCount := body.readCount
+		assert.NotEqual(t, 0, body.readCount)
+		assert.Equal(t, 1, body.closeCount)
+		assert.Nil(t, resp.content)
+		assert.Equal(t, contentFailed, resp.contentState)
+
+		// Second call should be no-op
+		resp.Body()
+		resp.chain.assertFailed(t)
+
+		assert.Equal(t, readCount, body.readCount)
+		assert.Equal(t, 1, body.closeCount)
+		assert.Nil(t, resp.content)
+		assert.Equal(t, contentFailed, resp.contentState)
+	})
+
+	t.Run("close error is remembered", func(t *testing.T) {
+		reporter := newMockReporter(t)
+
+		body := newMockBody("body string")
+		body.closeErr = errors.New("test error")
+
+		resp := NewResponse(reporter, &http.Response{
+			StatusCode: http.StatusOK,
+			Body:       body,
+		})
+
+		assert.Equal(t, 0, body.readCount)
+		assert.Equal(t, 0, body.closeCount)
+		assert.Nil(t, resp.content)
+		assert.Equal(t, contentPending, resp.contentState)
+
+		// Read body
+		resp.Body()
+		resp.chain.assertFailed(t)
+
+		readCount := body.readCount
+		assert.NotEqual(t, 0, body.readCount)
+		assert.Equal(t, 1, body.closeCount)
+		assert.Nil(t, resp.content)
+		assert.Equal(t, contentFailed, resp.contentState)
+
+		// Second call should be no-op
+		resp.Body()
+		resp.chain.assertFailed(t)
+
+		assert.Equal(t, readCount, body.readCount)
+		assert.Equal(t, 1, body.closeCount)
+		assert.Nil(t, resp.content)
+		assert.Equal(t, contentFailed, resp.contentState)
+	})
+}
+
 func TestResponse_Alias(t *testing.T) {
 	reporter := newMockReporter(t)
 
@@ -365,7 +488,7 @@ func TestResponse_Cookies(t *testing.T) {
 	c3 := resp.Cookie("baz")
 	resp.chain.assertFailed(t)
 	c3.chain.assertFailed(t)
-	assert.True(t, c3.Raw() == nil)
+	assert.Nil(t, c3.Raw())
 }
 
 func TestResponse_NoCookies(t *testing.T) {
@@ -387,7 +510,7 @@ func TestResponse_NoCookies(t *testing.T) {
 	c := resp.Cookie("foo")
 	resp.chain.assertFailed(t)
 	c.chain.assertFailed(t)
-	assert.True(t, c.Raw() == nil)
+	assert.Nil(t, c.Raw())
 }
 
 func TestResponse_Body(t *testing.T) {
@@ -418,7 +541,8 @@ func TestResponse_BodyClose(t *testing.T) {
 	resp := NewResponse(reporter, httpResp)
 
 	assert.Equal(t, "test_body", resp.Body().Raw())
-	assert.True(t, body.closed)
+	assert.NotEqual(t, 0, body.readCount)
+	assert.Equal(t, 1, body.closeCount)
 
 	resp.chain.assertNotFailed(t)
 }
@@ -436,10 +560,13 @@ func TestResponse_BodyError(t *testing.T) {
 		}
 
 		resp := NewResponse(reporter, httpResp)
+		respBody := resp.Body()
 
-		assert.Equal(t, "", resp.Body().Raw())
-		assert.True(t, body.closed)
+		assert.Equal(t, "", respBody.Raw())
+		assert.NotEqual(t, 0, body.readCount)
+		assert.Equal(t, 1, body.closeCount)
 
+		respBody.chain.assertFailed(t)
 		resp.chain.assertFailed(t)
 	})
 
@@ -455,7 +582,8 @@ func TestResponse_BodyError(t *testing.T) {
 		resp := NewResponse(reporter, httpResp)
 
 		assert.Equal(t, "", resp.Body().Raw())
-		assert.True(t, body.closed)
+		assert.NotEqual(t, 0, body.readCount)
+		assert.Equal(t, 1, body.closeCount)
 
 		resp.chain.assertFailed(t)
 	})
@@ -552,27 +680,49 @@ func TestResponse_NoContentNil(t *testing.T) {
 func TestResponse_NoContentFailure(t *testing.T) {
 	reporter := newMockReporter(t)
 
-	headers := map[string][]string{
-		"Content-Type": {"text/plain; charset=utf-8"},
-	}
+	t.Run("Content-Type not empty", func(t *testing.T) {
+		headers := map[string][]string{
+			"Content-Type": {"text/plain; charset=utf-8"},
+		}
 
-	body := ``
+		body := ``
 
-	httpResp := &http.Response{
-		StatusCode: http.StatusOK,
-		Header:     http.Header(headers),
-		Body:       ioutil.NopCloser(bytes.NewBufferString(body)),
-	}
+		httpResp := &http.Response{
+			StatusCode: http.StatusOK,
+			Header:     http.Header(headers),
+			Body:       ioutil.NopCloser(bytes.NewBufferString(body)),
+		}
 
-	resp := NewResponse(reporter, httpResp)
+		resp := NewResponse(reporter, httpResp)
 
-	assert.Equal(t, body, resp.Body().Raw())
-	resp.chain.assertNotFailed(t)
-	resp.chain.clearFailed()
+		assert.Equal(t, body, resp.Body().Raw())
+		resp.chain.assertNotFailed(t)
+		resp.chain.clearFailed()
 
-	resp.NoContent()
-	resp.chain.assertFailed(t)
-	resp.chain.clearFailed()
+		resp.NoContent()
+		resp.chain.assertFailed(t)
+		resp.chain.clearFailed()
+	})
+
+	t.Run("Read failure", func(t *testing.T) {
+		headers := map[string][]string{
+			"Content-Type": {""},
+		}
+
+		body := newMockBody("")
+		body.readErr = errors.New("test_error")
+
+		httpResp := &http.Response{
+			StatusCode: http.StatusOK,
+			Header:     http.Header(headers),
+			Body:       body,
+		}
+
+		resp := NewResponse(reporter, httpResp)
+
+		resp.NoContent()
+		resp.chain.assertFailed(t)
+	})
 }
 
 func TestResponse_ContentType(t *testing.T) {
@@ -763,6 +913,31 @@ func TestResponse_Text(t *testing.T) {
 	assert.Equal(t, "hello, world!", resp.Text().Raw())
 }
 
+func TestResponse_TextFailure(t *testing.T) {
+	reporter := newMockReporter(t)
+
+	headers := map[string][]string{
+		"Content-Type": {"text/plain; charset=utf-8"},
+	}
+
+	body := newMockBody(`hello, world!`)
+	body.readErr = errors.New("read error")
+
+	httpResp := &http.Response{
+		StatusCode: http.StatusOK,
+		Header:     http.Header(headers),
+		Body:       body,
+	}
+
+	resp := NewResponse(reporter, httpResp)
+
+	respText := resp.Text()
+
+	assert.Equal(t, "", respText.Raw())
+	respText.chain.assertFailed(t)
+	resp.chain.assertFailed(t)
+}
+
 func TestResponse_Form(t *testing.T) {
 	reporter := newMockReporter(t)
 
@@ -829,7 +1004,7 @@ func TestResponse_FormBadBody(t *testing.T) {
 	resp.chain.assertFailed(t)
 	resp.chain.clearFailed()
 
-	assert.True(t, resp.Form().Raw() == nil)
+	assert.Nil(t, resp.Form().Raw())
 }
 
 func TestResponse_FormBadType(t *testing.T) {
@@ -853,7 +1028,33 @@ func TestResponse_FormBadType(t *testing.T) {
 	resp.chain.assertFailed(t)
 	resp.chain.clearFailed()
 
-	assert.True(t, resp.Form().Raw() == nil)
+	assert.Nil(t, resp.Form().Raw())
+}
+
+func TestResponse_FormBodyReadFailure(t *testing.T) {
+	reporter := newMockReporter(t)
+
+	headers := map[string][]string{
+		"Content-Type": {"application/x-www-form-urlencoded"},
+	}
+
+	body := newMockBody("foo=bar")
+	body.readErr = errors.New("read error")
+
+	httpResp := &http.Response{
+		StatusCode: http.StatusOK,
+		Header:     http.Header(headers),
+		Body:       body,
+	}
+
+	resp := NewResponse(reporter, httpResp)
+
+	respForm := resp.Form()
+	assert.Nil(t, respForm.Raw())
+
+	respForm.chain.assertFailed(t)
+	resp.chain.assertFailed(t)
+	resp.chain.clearFailed()
 }
 
 func TestResponse_JSON(t *testing.T) {
@@ -918,7 +1119,7 @@ func TestResponse_JSONBadBody(t *testing.T) {
 	resp.chain.assertFailed(t)
 	resp.chain.clearFailed()
 
-	assert.True(t, resp.JSON().Raw() == nil)
+	assert.Nil(t, resp.JSON().Raw())
 }
 
 func TestResponse_JSONCharsetEmpty(t *testing.T) {
@@ -970,6 +1171,31 @@ func TestResponse_JSONCharsetBad(t *testing.T) {
 	assert.Equal(t, nil, resp.JSON().Raw())
 }
 
+func TestResponse_JSONReaderFailure(t *testing.T) {
+	reporter := newMockReporter(t)
+
+	headers := map[string][]string{
+		"Content-Type": {"application/json; charset=utf-8"},
+	}
+
+	body := newMockBody(`{"key": "value"}`)
+	body.readErr = errors.New("read error")
+
+	httpResp := &http.Response{
+		StatusCode: http.StatusOK,
+		Header:     http.Header(headers),
+		Body:       body,
+	}
+
+	resp := NewResponse(reporter, httpResp)
+
+	respJSON := resp.JSON()
+	assert.Nil(t, respJSON.Raw())
+	respJSON.chain.assertFailed(t)
+	resp.chain.assertFailed(t)
+	resp.chain.clearFailed()
+}
+
 func TestResponse_JSONP(t *testing.T) {
 	reporter := newMockReporter(t)
 
@@ -1048,7 +1274,7 @@ func TestResponse_JSONPBadBody(t *testing.T) {
 		resp.chain.assertFailed(t)
 		resp.chain.clearFailed()
 
-		assert.True(t, resp.JSONP("foo").Raw() == nil)
+		assert.Nil(t, resp.JSONP("foo").Raw())
 	}
 }
 
@@ -1101,6 +1327,31 @@ func TestResponse_JSONPCharsetBad(t *testing.T) {
 	assert.Nil(t, resp.JSONP("foo").Raw())
 }
 
+func TestResponse_JSONPBodyReadFailure(t *testing.T) {
+	reporter := newMockReporter(t)
+
+	headers := map[string][]string{
+		"Content-Type": {"application/javascript; charset=utf-8"},
+	}
+
+	body := newMockBody(`foo({"key": "value"})`)
+	body.readErr = errors.New("read error")
+
+	httpResp := &http.Response{
+		StatusCode: http.StatusOK,
+		Header:     http.Header(headers),
+		Body:       body,
+	}
+
+	resp := NewResponse(reporter, httpResp)
+
+	respJSONP := resp.JSONP("foo")
+	assert.Nil(t, respJSONP.Raw())
+	respJSONP.chain.assertFailed(t)
+	resp.chain.assertFailed(t)
+	resp.chain.clearFailed()
+}
+
 func TestResponse_ContentOpts(t *testing.T) {
 	reporter := newMockReporter(t)