1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
|
package jsonclient
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
)
// ContentType defines the default MIME type for JSON requests.
const ContentType = "application/json"
// HTTPClientErrorThreshold specifies the status code threshold for client errors (400+).
const HTTPClientErrorThreshold = 400
// ErrUnexpectedStatus indicates an unexpected HTTP response status.
var (
ErrUnexpectedStatus = errors.New("got unexpected HTTP status")
)
// DefaultClient provides a singleton JSON client using http.DefaultClient.
var DefaultClient = NewClient()
// Client wraps http.Client for JSON operations.
type client struct {
httpClient *http.Client
headers http.Header
indent string
}
// Get fetches a URL using GET and unmarshals the response into the provided object using DefaultClient.
func Get(url string, response any) error {
if err := DefaultClient.Get(url, response); err != nil {
return fmt.Errorf("getting JSON from %q: %w", url, err)
}
return nil
}
// Post sends a request as JSON and unmarshals the response into the provided object using DefaultClient.
func Post(url string, request any, response any) error {
if err := DefaultClient.Post(url, request, response); err != nil {
return fmt.Errorf("posting JSON to %q: %w", url, err)
}
return nil
}
// NewClient creates a new JSON client using the default http.Client.
func NewClient() Client {
return NewWithHTTPClient(http.DefaultClient)
}
// NewWithHTTPClient creates a new JSON client using the specified http.Client.
func NewWithHTTPClient(httpClient *http.Client) Client {
return &client{
httpClient: httpClient,
headers: http.Header{
"Content-Type": []string{ContentType},
},
}
}
// Headers returns the default headers for requests.
func (c *client) Headers() http.Header {
return c.headers
}
// Get fetches a URL using GET and unmarshals the response into the provided object.
func (c *client) Get(url string, response any) error {
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("creating GET request for %q: %w", url, err)
}
for key, val := range c.headers {
req.Header.Set(key, val[0])
}
res, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("executing GET request to %q: %w", url, err)
}
return parseResponse(res, response)
}
// Post sends a request as JSON and unmarshals the response into the provided object.
func (c *client) Post(url string, request any, response any) error {
var err error
var body []byte
if strReq, ok := request.(string); ok {
// If the request is a string, pass it through without serializing
body = []byte(strReq)
} else {
body, err = json.MarshalIndent(request, "", c.indent)
if err != nil {
return fmt.Errorf("marshaling request to JSON: %w", err)
}
}
req, err := http.NewRequestWithContext(
context.Background(),
http.MethodPost,
url,
bytes.NewReader(body),
)
if err != nil {
return fmt.Errorf("creating POST request for %q: %w", url, err)
}
for key, val := range c.headers {
req.Header.Set(key, val[0])
}
res, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("sending POST request to %q: %w", url, err)
}
return parseResponse(res, response)
}
// ErrorResponse checks if an error is a JSON error and unmarshals its body into the response.
func (c *client) ErrorResponse(err error, response any) bool {
var errMsg Error
if errors.As(err, &errMsg) {
return json.Unmarshal([]byte(errMsg.Body), response) == nil
}
return false
}
// parseResponse parses the HTTP response and unmarshals it into the provided object.
func parseResponse(res *http.Response, response any) error {
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if res.StatusCode >= HTTPClientErrorThreshold {
err = fmt.Errorf("%w: %v", ErrUnexpectedStatus, res.Status)
}
if err == nil {
err = json.Unmarshal(body, response)
}
if err != nil {
if body == nil {
body = []byte{}
}
return Error{
StatusCode: res.StatusCode,
Body: string(body),
err: err,
}
}
return nil
}
|