Skip to content

Commit 45b8314

Browse files
committed
🔥 feat: Add StreamResponseBody support for the Client
1 parent bc74824 commit 45b8314

File tree

8 files changed

+836
-17
lines changed

8 files changed

+836
-17
lines changed

client/client.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,20 @@ func (c *Client) DisableDebug() *Client {
526526
return c
527527
}
528528

529+
// StreamResponseBody returns the current StreamResponseBody setting.
530+
func (c *Client) StreamResponseBody() bool {
531+
return c.transport.StreamResponseBody()
532+
}
533+
534+
// SetStreamResponseBody enables or disables response body streaming.
535+
// When enabled, the response body can be read as a stream using BodyStream()
536+
// instead of being fully loaded into memory. This is useful for large responses
537+
// or server-sent events.
538+
func (c *Client) SetStreamResponseBody(enable bool) *Client {
539+
c.transport.SetStreamResponseBody(enable)
540+
return c
541+
}
542+
529543
// SetCookieJar sets the cookie jar for the client.
530544
func (c *Client) SetCookieJar(cookieJar *CookieJar) *Client {
531545
c.cookieJar = cookieJar

client/core.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,16 @@ func (c *core) execFunc() (*Response, error) {
8686
defer fasthttp.ReleaseRequest(reqv)
8787

8888
respv := fasthttp.AcquireResponse()
89-
defer fasthttp.ReleaseResponse(respv)
89+
defer func() {
90+
if respv != nil {
91+
fasthttp.ReleaseResponse(respv)
92+
}
93+
}()
9094

9195
c.req.RawRequest.CopyTo(reqv)
96+
if bodyStream := c.req.RawRequest.BodyStream(); bodyStream != nil {
97+
reqv.SetBodyStream(bodyStream, c.req.RawRequest.Header.ContentLength())
98+
}
9299

93100
var err error
94101
if cfg != nil {
@@ -115,7 +122,12 @@ func (c *core) execFunc() (*Response, error) {
115122
resp := AcquireResponse()
116123
resp.setClient(c.client)
117124
resp.setRequest(c.req)
118-
respv.CopyTo(resp.RawResponse)
125+
originalRaw := resp.RawResponse
126+
resp.RawResponse = respv
127+
respv = nil
128+
if originalRaw != nil {
129+
fasthttp.ReleaseResponse(originalRaw)
130+
}
119131
respChan <- resp
120132
}()
121133

client/core_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,13 @@ func (*blockingErrTransport) Client() any {
383383
return nil
384384
}
385385

386+
func (*blockingErrTransport) StreamResponseBody() bool {
387+
return false
388+
}
389+
390+
func (*blockingErrTransport) SetStreamResponseBody(_ bool) {
391+
}
392+
386393
func (b *blockingErrTransport) release() {
387394
b.releaseOnce.Do(func() { close(b.unblock) })
388395
}

client/response.go

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"path/filepath"
1212
"sync"
1313

14-
"github.com/gofiber/utils/v2"
14+
utils "github.com/gofiber/utils/v2"
1515
"github.com/valyala/fasthttp"
1616
)
1717

@@ -89,22 +89,38 @@ func (r *Response) Body() []byte {
8989
return r.RawResponse.Body()
9090
}
9191

92+
// BodyStream returns the response body as a stream reader.
93+
// Note: When using BodyStream(), the response body is not copied to memory,
94+
// so calling Body() afterwards may return an empty slice.
95+
func (r *Response) BodyStream() io.Reader {
96+
if stream := r.RawResponse.BodyStream(); stream != nil {
97+
return stream
98+
}
99+
// If streaming is not enabled, return a bytes.Reader from the regular body
100+
return bytes.NewReader(r.RawResponse.Body())
101+
}
102+
103+
// IsStreaming returns true if the response body is being streamed.
104+
func (r *Response) IsStreaming() bool {
105+
return r.RawResponse.BodyStream() != nil
106+
}
107+
92108
// String returns the response body as a trimmed string.
93109
func (r *Response) String() string {
94110
return utils.Trim(string(r.Body()), ' ')
95111
}
96112

97-
// JSON unmarshal the response body into the given interface{} using JSON.
113+
// JSON unmarshals the response body into the given interface{} using JSON.
98114
func (r *Response) JSON(v any) error {
99115
return r.client.jsonUnmarshal(r.Body(), v)
100116
}
101117

102-
// CBOR unmarshal the response body into the given interface{} using CBOR.
118+
// CBOR unmarshals the response body into the given interface{} using CBOR.
103119
func (r *Response) CBOR(v any) error {
104120
return r.client.cborUnmarshal(r.Body(), v)
105121
}
106122

107-
// XML unmarshal the response body into the given interface{} using XML.
123+
// XML unmarshals the response body into the given interface{} using XML.
108124
func (r *Response) XML(v any) error {
109125
return r.client.xmlUnmarshal(r.Body(), v)
110126
}
@@ -136,21 +152,30 @@ func (r *Response) Save(v any) error {
136152
}
137153
defer func() { _ = outFile.Close() }() //nolint:errcheck // not needed
138154

139-
if _, err = io.Copy(outFile, bytes.NewReader(r.Body())); err != nil {
155+
if r.IsStreaming() {
156+
_, err = io.Copy(outFile, r.BodyStream())
157+
} else {
158+
_, err = io.Copy(outFile, bytes.NewReader(r.Body()))
159+
}
160+
161+
if err != nil {
140162
return fmt.Errorf("failed to write response body to file: %w", err)
141163
}
142164

143165
return nil
144166

145167
case io.Writer:
146-
if _, err := io.Copy(p, bytes.NewReader(r.Body())); err != nil {
147-
return fmt.Errorf("failed to write response body to io.Writer: %w", err)
168+
var err error
169+
if r.IsStreaming() {
170+
_, err = io.Copy(p, r.BodyStream())
171+
} else {
172+
_, err = io.Copy(p, bytes.NewReader(r.Body()))
148173
}
149-
defer func() {
150-
if pc, ok := p.(io.WriteCloser); ok {
151-
_ = pc.Close() //nolint:errcheck // not needed
152-
}
153-
}()
174+
175+
if err != nil {
176+
return fmt.Errorf("failed to write response body to writer: %w", err)
177+
}
178+
154179
return nil
155180

156181
default:

0 commit comments

Comments
 (0)