-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrequest.go
403 lines (336 loc) · 9.9 KB
/
request.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
403
package request
import (
"bytes"
"compress/flate"
"compress/gzip"
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path"
"regexp"
"strings"
"time"
)
// urlPattern is the regular expression pattern for checking whether an URL is starting with HTTP
// or HTTPS protocol or not.
var urlPattern *regexp.Regexp = regexp.MustCompile(`^https?://.+`)
// request creates an HTTP request with the specific HTTP method, the request options, and the
// client config, and send it to the specific destination by the URL.
func (cli *Client) request(method, url string, opts ...RequestOptions) (*http.Response, error) {
var opt RequestOptions
if len(opts) > 0 {
opt = opts[0]
} else {
opt = RequestOptions{}
}
if method == "" {
method = opt.Method
}
req, canFunc, err := cli.makeRequest(method, url, opt)
if err != nil {
return nil, err
}
defer canFunc()
resp, err := cli.sendRequestWithInterceptors(req, opt)
if err != nil {
return resp, err
}
return cli.handleResponse(resp, opt)
}
// sendRequestWithInterceptors tries to execute the request and response interceptors and
// sends the request.
func (cli *Client) sendRequestWithInterceptors(
req *http.Request,
opt RequestOptions,
) (*http.Response, error) {
err := cli.doRequestIntercept(req)
if err != nil {
return nil, err
}
resp, err := cli.sendRequest(req, opt)
if err != nil {
return nil, err
}
err = cli.doResponseIntercept(resp)
if err != nil {
return resp, err
}
return resp, nil
}
// sendRequest gets an HTTP client from the HTTP clients pool and sends the request. It tries to
// re-send the request when it fails to make the request and the number of attempts is less than
// the maximum limitation.
func (cli *Client) sendRequest(req *http.Request, opt RequestOptions) (*http.Response, error) {
attempt := 0
maxAttempt := 1
if opt.MaxAttempt > 0 {
maxAttempt = opt.MaxAttempt
}
httpClient := cli.getHTTPClient(opt)
defer func() {
cli.clientPool.Put(httpClient)
}()
for {
attempt++
resp, err := httpClient.Do(req)
if err == nil || attempt >= maxAttempt {
return resp, err
} else if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return resp, err
}
}
}
// handleResponse handle the response that decompresses the body of the response if it was
// compressed, and validates the status code.
func (cli *Client) handleResponse(
resp *http.Response,
opt RequestOptions,
) (*http.Response, error) {
if !opt.DisableDecompress {
resp = cli.decodeResponseBody(resp)
}
return cli.validateResponse(resp, opt)
}
// decodeResponseBody tries to get the encoding type of the response's content, and decode
// (decompress) it if the response's body was compressed by `gzip` or `deflate`.
func (cli *Client) decodeResponseBody(resp *http.Response) *http.Response {
switch resp.Header.Get("Content-Encoding") {
case "deflate":
data, err := io.ReadAll(resp.Body)
if err != nil {
return resp
}
reader := flate.NewReader(bytes.NewReader(data))
resp.Body.Close()
resp.Body = reader
resp.Header.Del("Content-Encoding")
case "gzip", "x-gzip":
reader, err := gzip.NewReader(resp.Body)
if err != nil {
return resp
}
resp.Body.Close()
resp.Body = reader
resp.Header.Del("Content-Encoding")
}
return resp
}
// validateResponse validates the status code of the response, and returns fail if the result of
// the validation is false.
func (cli *Client) validateResponse(
resp *http.Response,
opt RequestOptions,
) (*http.Response, error) {
var validateStatus func(int) bool
if opt.ValidateStatus != nil {
validateStatus = opt.ValidateStatus
} else if cli.ValidateStatus != nil {
validateStatus = cli.ValidateStatus
} else {
validateStatus = cli.defaultValidateStatus
}
status := resp.StatusCode
ok := validateStatus(status)
if !ok {
return resp, fmt.Errorf("request failed with status code %d", status)
}
return resp, nil
}
// makeRequest creates a new `http.Request` object with the specific HTTP method, request url, and
// other configurations.
func (cli *Client) makeRequest(
method, url string,
opt RequestOptions,
) (*http.Request, context.CancelFunc, error) {
method, err := cli.getRequestMethod(method)
if err != nil {
return nil, nil, err
}
url, err = cli.parseURL(url, opt)
if err != nil {
return nil, nil, err
}
body, err := cli.getRequestBody(opt)
if err != nil {
return nil, nil, err
}
ctx, canFunc := cli.getContext(opt)
req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
canFunc()
return nil, nil, err
}
if err := cli.attachRequestHeaders(req, opt); err != nil {
canFunc()
return nil, nil, err
}
return req, canFunc, nil
}
// getRequestMethod validates and returns the HTTP method of the request. It'll return "GET" if the
// value of the method is empty.
func (cli *Client) getRequestMethod(method string) (string, error) {
if method == "" {
return http.MethodGet, nil
}
method = strings.ToUpper(method)
switch method {
case http.MethodConnect, http.MethodDelete, http.MethodGet, http.MethodHead, http.MethodOptions,
http.MethodPatch, http.MethodPost, http.MethodPut, http.MethodTrace:
return method, nil
default:
return "", ErrInvalidMethod
}
}
// attachRequestHeaders set the field values of the request headers by the request options or
// client configurations. It'll overwrite `Content-Type`, `User-Agent`, and other fields in the
// request headers by the config.
func (cli *Client) attachRequestHeaders(req *http.Request, opt RequestOptions) error {
cli.setHeaders(req, opt)
if err := cli.setContentType(req, opt); err != nil {
return err
}
cli.setUserAgent(req, opt)
if opt.Auth != nil {
req.SetBasicAuth(opt.Auth.Username, opt.Auth.Password)
}
return nil
}
// setHeaders set the field values of the request headers from the request options or the client
// configurations. The fields in the request options will overwrite the same fields in the client
// configuration.
func (cli *Client) setHeaders(req *http.Request, opt RequestOptions) {
if opt.Headers != nil {
for k, v := range opt.Headers {
for _, val := range v {
req.Header.Add(k, val)
}
}
}
if cli.Headers != nil {
for k, v := range cli.Headers {
if _, existed := req.Header[k]; existed {
continue
}
for _, val := range v {
req.Header.Add(k, val)
}
}
}
}
// setContentType checks the "Content-Type" field in the request headers, and set it by the
// "ContentType" field value from the request options if no value is set in the headers.
func (cli *Client) setContentType(req *http.Request, opt RequestOptions) error {
contentType := req.Header.Get("Content-Type")
if contentType != "" {
return nil
}
switch strings.ToLower(opt.ContentType) {
case RequestContentTypeJSON, "":
contentType = "application/json"
default:
return ErrUnsupportedType
}
req.Header.Set("Content-Type", contentType)
return nil
}
// setUserAgent checks the user agent value in the request options or the client configurations,
// and set it as the value of the `User-Agent` field in the request headers.
// Default "go-request/x.x".
func (cli *Client) setUserAgent(req *http.Request, opt RequestOptions) {
userAgent := opt.UserAgent
if userAgent == "" && cli.UserAgent != "" {
userAgent = cli.UserAgent
}
if userAgent == "" {
userAgent = RequestDefaultUserAgent
}
req.Header.Set("User-Agent", userAgent)
}
// parseURL gets the URL of the request and adds the parameters into the query of the request.
func (cli *Client) parseURL(uri string, opt RequestOptions) (string, error) {
baseURL, extraPath, err := cli.getURL(uri, opt)
if err != nil {
return "", err
}
obj, err := url.Parse(baseURL)
if err != nil {
return "", err
}
if extraPath != "" {
obj.Path = path.Join(obj.Path, extraPath)
}
obj.RawQuery = cli.getQueryParameters(obj.Query(), opt)
return obj.String(), nil
}
// getQueryParameters get the parameters of the request from the request options and the client's
// parameters.
func (cli *Client) getQueryParameters(query url.Values, opt RequestOptions) string {
if opt.Parameters != nil {
for k, vv := range opt.Parameters {
if !query.Has(k) {
query[k] = make([]string, 0, len(vv))
}
query[k] = append(query[k], vv...)
}
}
if cli.Parameters != nil {
for k, vv := range cli.Parameters {
if query.Has(k) {
continue
}
query[k] = make([]string, 0, len(vv))
query[k] = append(query[k], vv...)
}
}
if opt.ParametersSerializer != nil {
return opt.ParametersSerializer(query)
} else if cli.ParametersSerializer != nil {
return cli.ParametersSerializer(query)
}
return query.Encode()
}
// getURL returns the base url and extra path components from url parameter, optional config, and
// instance config.
func (cli *Client) getURL(url string, opt RequestOptions) (string, string, error) {
if url != "" && urlPattern.MatchString(url) {
return url, "", nil
}
baseURL := opt.BaseURL
if baseURL == "" && cli.BaseURL != "" {
baseURL = cli.BaseURL
}
if baseURL == "" {
baseURL = url
url = ""
}
if baseURL == "" {
return "", "", ErrNoURL
}
if !urlPattern.MatchString(baseURL) {
// prepend https as scheme if no scheme part in the url.
baseURL = "https://" + baseURL
}
return baseURL, url, nil
}
// getContext creates a Context by the request options or client settings, or returns the Context
// that is set in the request options.
func (cli *Client) getContext(opt RequestOptions) (context.Context, context.CancelFunc) {
if opt.Context != nil {
return opt.Context, func() {} // empty cancel function, just do nothing
}
baseCtx := context.Background()
timeout := RequestTimeoutDefault
if opt.Timeout > 0 || opt.Timeout == RequestTimeoutNoLimit {
timeout = opt.Timeout
} else if cli.Timeout > 0 || cli.Timeout == RequestTimeoutNoLimit {
timeout = cli.Timeout
}
if timeout == RequestTimeoutNoLimit {
return baseCtx, func() {} // empty cancel function, just do nothing
} else {
return context.WithTimeout(baseCtx, time.Duration(timeout)*time.Millisecond)
}
}