WithMaxResponseBody wrapped the body in io.LimitedReader, which returns EOF at the cap, so Bytes/JSON/XML silently returned a truncated body with a nil error despite the documented contract. Read one byte past the limit and return the new ErrResponseTooLarge sentinel when exceeded; bodies exactly at the limit still succeed.
124 lines
2.8 KiB
Go
124 lines
2.8 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 enforces a maximum number of bytes that may be read from
|
|
// a response body. Reading more than limit bytes returns ErrResponseTooLarge
|
|
// rather than silently truncating the body. The original body is closed via
|
|
// the separate Closer.
|
|
type limitedReadCloser struct {
|
|
r io.Reader // an io.LimitReader over the original body (limit+1 bytes)
|
|
c io.Closer // the original body, for Close
|
|
limit int64
|
|
read int64
|
|
}
|
|
|
|
func (l *limitedReadCloser) Read(p []byte) (int, error) {
|
|
n, err := l.r.Read(p)
|
|
l.read += int64(n)
|
|
if l.read > l.limit {
|
|
return n, ErrResponseTooLarge
|
|
}
|
|
return n, err
|
|
}
|
|
|
|
func (l *limitedReadCloser) Close() error {
|
|
return l.c.Close()
|
|
}
|