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
package httpjson
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
)
const (
DefaultLimit int64 = 8 << 20
Unlimited int64 = -1
statusDetailLimit = 300
)
var ErrTooLarge = errors.New("httpjson: the response body is too large")
type Response struct {
Status int
Header http.Header
Body []byte
}
type StatusError struct {
Method string
URL string
Status int
Body []byte
}
func (e *StatusError) HTTPStatus() int { return e.Status }
func (e *StatusError) Error() string {
text := strings.TrimSpace(string(e.Body))
if len(text) > statusDetailLimit {
text = text[:statusDetailLimit] + "..."
}
if text == "" {
return fmt.Sprintf("%s %s: HTTP %d", e.Method, e.URL, e.Status)
}
return fmt.Sprintf("%s %s: HTTP %d: %s", e.Method, e.URL, e.Status, text)
}
type TooLargeError struct {
Status int
Limit int64
}
func (e *TooLargeError) Error() string {
return fmt.Sprintf("httpjson: the HTTP %d response is larger than %d bytes", e.Status, e.Limit)
}
func (e *TooLargeError) Is(target error) bool { return target == ErrTooLarge }
func NewRequest(ctx context.Context, method, target string, in any) (*http.Request, error) {
var body io.Reader
if in != nil {
raw, err := json.Marshal(in)
if err != nil {
return nil, fmt.Errorf("httpjson: encoding %s %s: %w", method, target, err)
}
body = bytes.NewReader(raw)
}
req, err := http.NewRequestWithContext(ctx, method, target, body)
if err != nil {
return nil, err
}
if in != nil {
req.Header.Set("Content-Type", "application/json")
}
return req, nil
}
func Exchange(ctx context.Context, client *http.Client, req *http.Request, limit int64) (Response, error) {
if client == nil {
client = http.DefaultClient
}
if ctx != nil && ctx != req.Context() {
req = req.WithContext(ctx)
}
if limit == 0 {
limit = DefaultLimit
}
resp, err := client.Do(req)
if err != nil {
return Response{}, err
}
defer resp.Body.Close()
var reader io.Reader = resp.Body
if limit > 0 {
reader = io.LimitReader(resp.Body, limit+1)
}
raw, err := io.ReadAll(reader)
if err != nil {
return Response{}, err
}
if limit > 0 && int64(len(raw)) > limit {
return Response{}, &TooLargeError{Status: resp.StatusCode, Limit: limit}
}
return Response{Status: resp.StatusCode, Header: resp.Header, Body: raw}, nil
}
func Do[Out any](ctx context.Context, client *http.Client, req *http.Request) (Out, error) {
return DoLimit[Out](ctx, client, req, DefaultLimit)
}
func DoLimit[Out any](ctx context.Context, client *http.Client, req *http.Request, limit int64) (Out, error) {
var out Out
resp, err := Exchange(ctx, client, req, limit)
if err != nil {
return out, err
}
if resp.Status < 200 || resp.Status > 299 {
return out, &StatusError{Method: req.Method, URL: req.URL.Redacted(), Status: resp.Status, Body: resp.Body}
}
switch target := any(&out).(type) {
case *Response:
*target = resp
return out, nil
case *[]byte:
*target = resp.Body
return out, nil
}
if len(bytes.TrimSpace(resp.Body)) == 0 {
return out, nil
}
if err := json.Unmarshal(resp.Body, &out); err != nil {
return out, fmt.Errorf("httpjson: decoding %s %s: %w", req.Method, req.URL.Redacted(), err)
}
return out, nil
}