Skip to content

Commit c1fa358

Browse files
authored
Don't use sync.Pool for json.NewEncoder target buffers (#421)
* Don't release buf to sync.Pool to early The slice returned by `noescapeJSONMarshal` continues to be accessed even after `pool.Put` was called on the buffer. This might lead to incorrect data being sent in request body if the buffer is acquired and re-written by a concurrently running goroutine. So make sure we release the buffer once the request completes. * Release request body buf back to sync.Pool only after body is closed net/http.RoundTripper may access request body in a separate goroutine, so we need to wait release the buf back to sync.Pool only after the body is closed. From the docs: // RoundTrip must always close the body, including on errors, // but depending on the implementation may do so in a separate // goroutine even after RoundTrip returns. This means that // callers wanting to reuse the body for subsequent requests // must arrange to wait for the Close call before doing so.
1 parent d717652 commit c1fa358

File tree

4 files changed

+50
-11
lines changed

4 files changed

+50
-11
lines changed

client.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -869,7 +869,6 @@ func (c *Client) GetClient() *http.Client {
869869
// Executes method executes the given `Request` object and returns response
870870
// error.
871871
func (c *Client) execute(req *Request) (*Response, error) {
872-
defer releaseBuffer(req.bodyBuf)
873872
// Apply Request middleware
874873
var err error
875874

@@ -903,6 +902,8 @@ func (c *Client) execute(req *Request) (*Response, error) {
903902
return nil, wrapNoRetryErr(err)
904903
}
905904

905+
req.RawRequest.Body = newRequestBodyReleaser(req.RawRequest.Body, req.bodyBuf)
906+
906907
req.Time = time.Now()
907908
resp, err := c.httpClient.Do(req.RawRequest)
908909

middleware.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ func handleRequestBody(c *Client, r *Request) (err error) {
458458
bodyBytes = []byte(s)
459459
} else if IsJSONType(contentType) &&
460460
(kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) {
461-
bodyBytes, err = jsonMarshal(c, r, r.Body)
461+
r.bodyBuf, err = jsonMarshal(c, r, r.Body)
462462
if err != nil {
463463
return
464464
}

request.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -870,11 +870,14 @@ func (r *Request) initValuesMap() {
870870
}
871871
}
872872

873-
var noescapeJSONMarshal = func(v interface{}) ([]byte, error) {
873+
var noescapeJSONMarshal = func(v interface{}) (*bytes.Buffer, error) {
874874
buf := acquireBuffer()
875-
defer releaseBuffer(buf)
876875
encoder := json.NewEncoder(buf)
877876
encoder.SetEscapeHTML(false)
878-
err := encoder.Encode(v)
879-
return buf.Bytes(), err
877+
if err := encoder.Encode(v); err != nil {
878+
releaseBuffer(buf)
879+
return nil, err
880+
}
881+
882+
return buf, nil
880883
}

util.go

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"runtime"
2020
"sort"
2121
"strings"
22+
"sync"
2223
)
2324

2425
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
@@ -139,13 +140,19 @@ type ResponseLog struct {
139140
//_______________________________________________________________________
140141

141142
// way to disable the HTML escape as opt-in
142-
func jsonMarshal(c *Client, r *Request, d interface{}) ([]byte, error) {
143-
if !r.jsonEscapeHTML {
144-
return noescapeJSONMarshal(d)
145-
} else if !c.jsonEscapeHTML {
143+
func jsonMarshal(c *Client, r *Request, d interface{}) (*bytes.Buffer, error) {
144+
if !r.jsonEscapeHTML || !c.jsonEscapeHTML {
146145
return noescapeJSONMarshal(d)
147146
}
148-
return c.JSONMarshal(d)
147+
148+
data, err := c.JSONMarshal(d)
149+
if err != nil {
150+
return nil, err
151+
}
152+
153+
buf := acquireBuffer()
154+
_, _ = buf.Write(data)
155+
return buf, nil
149156
}
150157

151158
func firstNonEmpty(v ...string) string {
@@ -283,6 +290,34 @@ func releaseBuffer(buf *bytes.Buffer) {
283290
}
284291
}
285292

293+
// requestBodyReleaser wraps requests's body and implements custom Close for it.
294+
// The Close method closes original body and releases request body back to sync.Pool.
295+
type requestBodyReleaser struct {
296+
releaseOnce sync.Once
297+
reqBuf *bytes.Buffer
298+
io.ReadCloser
299+
}
300+
301+
func newRequestBodyReleaser(respBody io.ReadCloser, reqBuf *bytes.Buffer) io.ReadCloser {
302+
if reqBuf == nil {
303+
return respBody
304+
}
305+
306+
return &requestBodyReleaser{
307+
reqBuf: reqBuf,
308+
ReadCloser: respBody,
309+
}
310+
}
311+
312+
func (rr *requestBodyReleaser) Close() error {
313+
err := rr.ReadCloser.Close()
314+
rr.releaseOnce.Do(func() {
315+
releaseBuffer(rr.reqBuf)
316+
})
317+
318+
return err
319+
}
320+
286321
func closeq(v interface{}) {
287322
if c, ok := v.(io.Closer); ok {
288323
silently(c.Close())

0 commit comments

Comments
 (0)