Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions response.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ func (r *Response) readAll() (err error) {
} else {
r.bodyBytes, err = ioReadAll(r.Body)
closeq(r.Body)
r.Body = &nopReadCloser{r: bytes.NewReader(r.bodyBytes)}
r.Body = &nopReadCloser{r: bytes.NewReader(r.bodyBytes), resetOnEOF: true}
}
if err == io.ErrUnexpectedEOF {
// content-encoding scenario's - empty/no response body from server
Expand Down Expand Up @@ -265,7 +265,7 @@ func (r *Response) wrapCopyReadCloser() {
f: func(b *bytes.Buffer) {
r.bodyBytes = append([]byte{}, b.Bytes()...)
closeq(r.Body)
r.Body = &nopReadCloser{r: bytes.NewReader(r.bodyBytes)}
r.Body = &nopReadCloser{r: bytes.NewReader(r.bodyBytes), resetOnEOF: true}
releaseBuffer(b)
},
}
Expand Down
34 changes: 32 additions & 2 deletions stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,30 @@ func encodeJSONEscapeHTMLIndent(w io.Writer, v any, esc bool, indent string) err

func decodeJSON(r io.Reader, v any) error {
dec := json.NewDecoder(r)

// Handle nopReadCloser specially to support multiple JSON objects
// while preventing infinite loops
if nrc, ok := r.(*nopReadCloser); ok {
// Temporarily disable auto-reset to prevent infinite loops
originalReset := nrc.resetOnEOF
nrc.resetOnEOF = false
defer func() { nrc.resetOnEOF = originalReset }()

// Decode all JSON objects in the data
for {
if err := dec.Decode(v); err == io.EOF {
break
} else if err != nil {
return err
}
}

// After decoding, reset for future reads
nrc.Reset()
return nil
}

// For other readers, decode multiple JSON objects as intended
for {
if err := dec.Decode(v); err == io.EOF {
break
Expand Down Expand Up @@ -196,19 +220,25 @@ func (r *copyReadCloser) Close() error {
var _ io.ReadCloser = (*nopReadCloser)(nil)

type nopReadCloser struct {
r *bytes.Reader
r *bytes.Reader
resetOnEOF bool // Whether to reset on EOF
}

func (r *nopReadCloser) Read(p []byte) (int, error) {
n, err := r.r.Read(p)
if err == io.EOF {
if err == io.EOF && r.resetOnEOF {
r.r.Seek(0, io.SeekStart)
}
return n, err
}

func (r *nopReadCloser) Close() error { return nil }

// Reset allows manual reset of the reader position
func (r *nopReadCloser) Reset() {
r.r.Seek(0, io.SeekStart)
}

var _ flate.Reader = (*nopReader)(nil)

type nopReader struct{}
Expand Down
134 changes: 134 additions & 0 deletions stream_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package resty

import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"testing"
)

func TestDecodeJSONWhenResponseBodyIsNull(t *testing.T) {
r := &Response{
Body: io.NopCloser(bytes.NewReader([]byte("null"))),
}
r.wrapCopyReadCloser()
err := r.readAll()
assertNil(t, err)

var result map[int]int
err = decodeJSON(r.Body, &result)
assertNil(t, err)
assertNil(t, result)
}

func TestGetMethodWhenResponseIsNull(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("null"))
}))

client := New().SetRetryCount(3).EnableGenerateCurlCmd()

var x any
resp, err := client.R().SetBody("{}").
SetHeader("Content-Type", "application/json; charset=utf-8").
SetForceResponseContentType("application/json").
SetAllowMethodGetPayload(true).
SetResponseBodyUnlimitedReads(true).
SetResult(&x).
Get(server.URL + "/test")

assertNil(t, err)
assertEqual(t, "null", resp.String())
assertEqual(t, nil, x)
}

func TestDecodeJSON(t *testing.T) {
// Test single object
jsonData := `{"name": "John", "age": 30}`
reader := bytes.NewReader([]byte(jsonData))
var result map[string]any
err := decodeJSON(reader, &result)
assertNil(t, err)
assertEqual(t, "John", result["name"])
assertEqual(t, float64(30), result["age"])

// Test multiple objects - should get the last one
multipleJSON := `{"id": 1}
{"id": 2}
{"id": 3}`
reader2 := bytes.NewReader([]byte(multipleJSON))
var result2 map[string]any
err = decodeJSON(reader2, &result2)
assertNil(t, err)
assertEqual(t, float64(3), result2["id"])

// Test malformed JSON
malformedJSON := `{"name": "John", "age":}`
reader3 := bytes.NewReader([]byte(malformedJSON))
var result3 map[string]any
err = decodeJSON(reader3, &result3)
assertNotNil(t, err)
}

func TestWrapCopyReadCloser(t *testing.T) {
testData := "Hello, World!"
r := &Response{
Body: io.NopCloser(bytes.NewReader([]byte(testData))),
}

// Before wrapping, bodyBytes should be empty
assertEqual(t, 0, len(r.bodyBytes))

r.wrapCopyReadCloser()

// Read data - should trigger copy mechanism and transform to nopReadCloser
data, err := io.ReadAll(r.Body)
assertNil(t, err)
assertEqual(t, testData, string(data))
assertEqual(t, testData, string(r.bodyBytes))

// Should now be nopReadCloser for unlimited reads
_, ok := r.Body.(*nopReadCloser)
assertEqual(t, true, ok)

// Test unlimited reads
data2, err := io.ReadAll(r.Body)
assertNil(t, err)
assertEqual(t, testData, string(data2))
}

func TestMultipleJSONObjectsSupport(t *testing.T) {
// Test multiple JSON objects with wrapCopyReadCloser
jsonData := `{"first": 1}
{"second": 2}
{"third": 3}`

r := &Response{
Body: io.NopCloser(bytes.NewReader([]byte(jsonData))),
}
r.wrapCopyReadCloser()

// Should process all objects and get the last one
var result map[string]any
err := decodeJSON(r.Body, &result)
assertNil(t, err)
assertEqual(t, float64(3), result["third"])

// Should support unlimited reads and decoding
var result2 map[string]any
err = decodeJSON(r.Body, &result2)
assertNil(t, err)
assertEqual(t, float64(3), result2["third"])

// Test direct nopReadCloser usage
nopReader := &nopReadCloser{
r: bytes.NewReader([]byte(jsonData)),
resetOnEOF: true,
}

var result3 map[string]any
err = decodeJSON(nopReader, &result3)
assertNil(t, err)
assertEqual(t, float64(3), result3["third"])
}