-
Notifications
You must be signed in to change notification settings - Fork 0
/
util.go
402 lines (339 loc) · 8.86 KB
/
util.go
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
package resty
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/goccy/go-json"
"github.com/pubgo/funk/convert"
"github.com/pubgo/funk/result"
"github.com/pubgo/funk/retry"
"github.com/valyala/bytebufferpool"
"github.com/valyala/fasthttp"
"github.com/valyala/fasttemplate"
"golang.org/x/net/http/httpguts"
"github.com/pubgo/lava/lava"
"github.com/pubgo/lava/pkg/httputil"
)
func do(cfg *Config) lava.HandlerFunc {
client := cfg.Build()
return func(ctx context.Context, req lava.Request) (lava.Response, error) {
r := req.(*requestImpl).req
defer fasthttp.ReleaseRequest(r.req)
var err error
resp := fasthttp.AcquireResponse()
handle := func() error {
deadline, ok := ctx.Deadline()
if ok {
err = client.DoDeadline(r.req, resp, deadline)
} else {
err = client.Do(r.req, resp)
}
return err
}
if r.backoff != nil {
err = retry.New(r.backoff).Do(func(i int) error { return handle() })
} else {
err = handle()
}
if err != nil {
return nil, err
}
return &responseImpl{resp: resp}, nil
}
}
func getBodyReader(rawBody interface{}) ([]byte, error) {
switch body := rawBody.(type) {
case nil:
return nil, nil
case []byte:
return body, nil
case string:
return convert.StoB(body), nil
case *bytes.Buffer:
return body.Bytes(), nil
// We prioritize *bytes.Reader here because we don't really want to
// deal with it seeking so want it to match here instead of the
// io.ReadSeeker case.
case *bytes.Reader:
buf, err := io.ReadAll(body)
if err != nil {
return nil, err
}
return buf, nil
// Compat case
case io.ReadSeeker:
_, err := body.Seek(0, 0)
if err != nil {
return nil, err
}
buf, err := io.ReadAll(body)
if err != nil {
return nil, err
}
return buf, nil
// Read all in so we can reset
case io.Reader:
buf, err := io.ReadAll(body)
if err != nil {
return nil, err
}
return buf, nil
case url.Values:
return convert.StoB(body.Encode()), nil
case json.Marshaler:
return body.MarshalJSON()
default:
bb := bytebufferpool.Get()
defer bytebufferpool.Put(bb)
if err := json.NewEncoder(bb).Encode(rawBody); err != nil {
return nil, err
}
return bb.Bytes(), nil
}
}
// IsRedirect returns true if the status code indicates a redirect.
func IsRedirect(statusCode int) bool {
return statusCode == http.StatusMovedPermanently ||
statusCode == http.StatusFound ||
statusCode == http.StatusSeeOther ||
statusCode == http.StatusTemporaryRedirect ||
statusCode == http.StatusPermanentRedirect
}
func handleHeader(c *Client, req *Request) {
header := c.cfg.DefaultHeader
if header != nil {
for k, v := range header {
req.header.Add(k, v)
}
}
}
func handlePath(c *Client, req *Request) (path string, err error) {
reqConf := req.cfg
reqUrl := c.baseUrl.JoinPath(reqConf.Path)
req.operation = reqUrl.Path
path = reqUrl.Path
if v, ok := c.pathTemplates.Load(reqUrl.Path); ok {
if v != nil {
path, err = pathTemplateRun(v.(*fasttemplate.Template), req.params)
if err != nil {
return
}
}
} else {
if regParam.MatchString(reqUrl.Path) {
pathTemplate, err := fasttemplate.NewTemplate(reqUrl.Path, "{", "}")
if err != nil {
return "", err
}
c.pathTemplates.Store(reqUrl.Path, pathTemplate)
} else {
c.pathTemplates.Store(reqUrl.Path, nil)
}
}
return
}
func handleContentType(c *Client, req *Request) (string, error) {
defaultConf := c.cfg
reqConf := req.cfg
contentType := defaultContentType
if defaultConf.DefaultContentType != "" {
contentType = defaultConf.DefaultContentType
}
if reqConf.ContentType != "" {
contentType = reqConf.ContentType
}
if req.contentType != "" {
contentType = req.contentType
}
if contentType == "" {
return "", errors.New("context-type header is empty")
}
return contentType, nil
}
// doRequest data:[bytes|string|map|struct]
func doRequest(ctx context.Context, c *Client, req *Request) (rsp result.Result[*fasthttp.Request]) {
if ctx == nil {
ctx = context.Background()
}
r := fasthttp.AcquireRequest()
ct, err := handleContentType(c, req)
if err != nil {
return rsp.WithErr(err)
}
r.Header.Set(httputil.HeaderContentType, ct)
path, err := handlePath(c, req)
if err != nil {
return rsp.WithErr(err)
}
r.SetRequestURI(path)
mth := req.cfg.Method
if mth == "" {
return rsp.WithErr(fmt.Errorf("http method is empty"))
}
r.Header.SetMethod(mth)
bodyRaw, err := getBodyReader(req.body)
if err != nil {
return rsp.WithErr(err)
}
r.SetBodyRaw(bodyRaw)
handleHeader(c, req)
for k, v := range req.header {
for i := range v {
r.Header.Add(k, v[i])
}
}
// enable auth
if c.cfg.EnableAuth || req.cfg.EnableAuth {
if c.cfg.BasicToken != "" {
r.Header.Set("Authentication", "Basic "+c.cfg.BasicToken)
}
if c.cfg.JwtToken != "" {
r.Header.Set("Authentication", "Bearer "+c.cfg.JwtToken)
}
}
uri := fasthttp.AcquireURI()
defer fasthttp.ReleaseURI(uri)
uri.SetScheme(c.baseUrl.Scheme)
uri.SetHost(c.baseUrl.Host)
uri.SetPath(path)
if req.query != nil {
uri.SetQueryString(req.query.Encode())
}
r.SetURI(uri)
if req.backoff == nil {
if c.backoff != nil {
req.backoff = c.backoff
}
if req.cfg.Backoff != nil {
req.backoff = req.cfg.Backoff
}
}
return rsp.WithVal(r)
}
func filterFlags(content string) string {
for i, char := range content {
if char == ' ' || char == ';' {
return content[:i]
}
}
return content
}
func toString(v any) string {
switch t := v.(type) {
case string:
return t
case bool:
return strconv.FormatBool(t)
case int:
return strconv.Itoa(t)
case int8:
return strconv.FormatInt(int64(t), 10)
case int16:
return strconv.FormatInt(int64(t), 10)
case int32:
return strconv.FormatInt(int64(t), 10)
case int64:
return strconv.FormatInt(int64(t), 10)
case uint:
return strconv.FormatUint(uint64(t), 10)
case uint8:
return strconv.FormatUint(uint64(t), 10)
case uint16:
return strconv.FormatUint(uint64(t), 10)
case uint32:
return strconv.FormatUint(uint64(t), 10)
case uint64:
return strconv.FormatUint(uint64(t), 10)
default:
return fmt.Sprintf("%v", t)
}
}
func pathTemplateRun(tpl *fasttemplate.Template, params map[string]any) (string, error) {
return tpl.ExecuteFuncStringWithErr(func(w io.Writer, tag string) (int, error) {
return w.Write(convert.StoB(toString(params[tag])))
})
}
// get is like Get, but key must already be in CanonicalHeaderKey form.
func headerGet(h http.Header, key string) string {
if v := h[key]; len(v) > 0 {
return v[0]
}
return ""
}
// has reports whether h has the provided key defined, even if it's
// set to 0-length slice.
func headerHas(h http.Header, key string) bool {
_, ok := h[key]
return ok
}
// Given a string of the form "host", "host:port", or "[ipv6::address]:port",
// return true if the string includes a port.
func hasPort(s string) bool { return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") }
// removeEmptyPort strips the empty port in ":port" to ""
// as mandated by RFC 3986 Section 6.2.3.
func removeEmptyPort(host string) string {
if hasPort(host) {
return strings.TrimSuffix(host, ":")
}
return host
}
func isNotToken(r rune) bool {
return !httpguts.IsTokenRune(r)
}
func validMethod(method string) bool {
return len(method) > 0 && strings.IndexFunc(method, isNotToken) == -1
}
func closeBody(r *http.Request) error {
if r.Body == nil {
return nil
}
return r.Body.Close()
}
// requestBodyReadError wraps an error from (*Request).write to indicate
// that the error came from a Read call on the Request.Body.
// This error type should not escape the net/http package to users.
type requestBodyReadError struct{ error }
// Return value if nonempty, def otherwise.
func valueOrDefault(value, def string) string {
if value != "" {
return value
}
return def
}
// errMissingHost is returned by Write when there is no Host or URL present in
// the Request.
var errMissingHost = errors.New("http: Request.Write on Request with no Host or URL set")
func closeRequestBody(r *http.Request) error {
if r.Body == nil {
return nil
}
return r.Body.Close()
}
// Headers that Request.Write handles itself and should be skipped.
var reqWriteExcludeHeader = map[string]bool{
"Host": true, // not in Header map anyway
"User-Agent": true,
"Content-Length": true,
"Transfer-Encoding": true,
"Trailer": true,
}
// requestMethodUsuallyLacksBody reports whether the given request
// method is one that typically does not involve a request body.
// This is used by the Transport (via
// transferWriter.shouldSendChunkedRequestBody) to determine whether
// we try to test-read a byte from a non-nil Request.Body when
// Request.outgoingLength() returns -1. See the comments in
// shouldSendChunkedRequestBody.
func requestMethodUsuallyLacksBody(method string) bool {
switch method {
case "GET", "HEAD", "DELETE", "OPTIONS", "PROPFIND", "SEARCH":
return true
}
return false
}