Add Client with response wrapper, request helpers, and full middleware assembly
Implements the top-level httpx.Client that composes the full chain: Logging → User Middlewares → Retry → Circuit Breaker → Balancer → Transport - Response wrapper with JSON/XML/Bytes decoding and body caching - NewJSONRequest helper with Content-Type and GetBody support - Functional options: WithBaseURL, WithTimeout, WithRetry, WithEndpoints, etc. - Integration tests covering retry, balancing, error mapping, and JSON round-trips
This commit is contained in:
99
response.go
Normal file
99
response.go
Normal file
@@ -0,0 +1,99 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user