Files
httpx/response.go
Aleksey Shakhmatov 21274c178a Add WithMaxResponseBody option to prevent client-side OOM
Wraps response body with io.LimitedReader when configured, preventing
unbounded reads from io.ReadAll in Response.Bytes(). Protects against
upstream services returning unexpectedly large responses.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 21:48:05 +03:00

115 lines
2.4 KiB
Go

package httpx
import (
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"net/http"
)
// Response wraps http.Response with convenience methods.
type Response struct {
*http.Response
body []byte
read bool
}
func newResponse(resp *http.Response) *Response {
return &Response{Response: resp}
}
// Bytes reads and returns the entire response body.
// The body is cached so subsequent calls return the same data.
func (r *Response) Bytes() ([]byte, error) {
if r.read {
return r.body, nil
}
defer r.Body.Close()
b, err := io.ReadAll(r.Body)
if err != nil {
return nil, err
}
r.body = b
r.read = true
return b, nil
}
// String reads the response body and returns it as a string.
func (r *Response) String() (string, error) {
b, err := r.Bytes()
if err != nil {
return "", err
}
return string(b), nil
}
// JSON decodes the response body as JSON into v.
func (r *Response) JSON(v any) error {
b, err := r.Bytes()
if err != nil {
return fmt.Errorf("httpx: reading response body: %w", err)
}
if err := json.Unmarshal(b, v); err != nil {
return fmt.Errorf("httpx: decoding JSON: %w", err)
}
return nil
}
// XML decodes the response body as XML into v.
func (r *Response) XML(v any) error {
b, err := r.Bytes()
if err != nil {
return fmt.Errorf("httpx: reading response body: %w", err)
}
if err := xml.Unmarshal(b, v); err != nil {
return fmt.Errorf("httpx: decoding XML: %w", err)
}
return nil
}
// IsSuccess returns true if the status code is in the 2xx range.
func (r *Response) IsSuccess() bool {
return r.StatusCode >= 200 && r.StatusCode < 300
}
// IsError returns true if the status code is 4xx or 5xx.
func (r *Response) IsError() bool {
return r.StatusCode >= 400
}
// Close drains and closes the response body.
func (r *Response) Close() error {
if r.read {
return nil
}
_, _ = io.Copy(io.Discard, r.Body)
return r.Body.Close()
}
// BodyReader returns a reader for the response body.
// If the body has already been read via Bytes/String/JSON/XML,
// returns a reader over the cached bytes.
func (r *Response) BodyReader() io.Reader {
if r.read {
return bytes.NewReader(r.body)
}
return r.Body
}
// limitedReadCloser wraps an io.LimitedReader with a separate Closer
// so the original body can be closed.
type limitedReadCloser struct {
R io.LimitedReader
C io.Closer
}
func (l *limitedReadCloser) Read(p []byte) (int, error) {
return l.R.Read(p)
}
func (l *limitedReadCloser) Close() error {
return l.C.Close()
}