This repository has been archived by the owner on May 14, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
/
options.go
247 lines (203 loc) · 7.22 KB
/
options.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
package gotcha
import (
"encoding/json"
"github.com/Sleeyax/urlValues"
"github.com/imdario/mergo"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
"time"
)
var RedirectStatusCodes = []int{300, 301, 302, 303, 304, 307, 308}
type JSON map[string]interface{}
type UnmarshalJsonFunc = func(data []byte) (JSON, error)
type MarshalJsonFunc = func(json JSON) ([]byte, error)
type RedirectOptions struct {
// Specifies if redirects should be rewritten as GET.
//
// If false, when sending a POST request and receiving a 302,
// it will resend the body to the new location using the same HTTP method (POST in this case).
//
// Note that if a 303 is sent by the server in response to any request type (POST, DELETE, etc.),
// gotcha will automatically request the resource pointed to in the location header via GET.
// This is in accordance with the spec https://tools.ietf.org/html/rfc7231#section-6.4.4.
RewriteMethods bool
// Maximum amount of redirects to follow.
// Follows an unlimited amount of redirects when set to 0.
Limit int
}
type Options struct {
// Adapter is an adapter that will be used by gotcha to make the actual request.
// Implement your own Adapter or use the RequestAdapter to get started.
Adapter Adapter
// Request URI.
// Can be relative or absolute.
URI string
// FullUrl is the URI that was computed form PrefixURL and URI.
// You shouldn't need to modify this in most cases.
FullUrl *url.URL
// Proxy URL.
// If this is an authenticated Proxy, make sure Username and Password are set.
Proxy *url.URL
// Retry on failure.
Retry bool
// Additional configuration Options for Retry.
RetryOptions *RetryOptions
// Amount of retries that have been done so far.
retries int
// The HTTP method used to make the request.
Method string
// When specified, prefixUrl will be prepended to the url.
// The prefix can be any valid URI, either relative or absolute.
// A trailing slash / is optional - one will be added automatically.
PrefixURL string
// Request headers.
Headers http.Header
// Request Body.
//
// Body will be set in the following order,
// whichever value is found to be of non-zero value first: Form -> Json -> Body.
// Raw body content.
Body io.ReadCloser
// JSON data.
Json JSON
// Form data that will be converted to a query string.
Form urlValues.Values
// A function used to parse JSON responses.
UnmarshalJson UnmarshalJsonFunc
// A function used to stringify the body of JSON requests.
MarshalJson MarshalJsonFunc
// Can contain custom user data.
// This can be useful for storing authentication tokens for example.
Context interface{}
// CokieJar automatically stores & parses cookies.
//
// The CookieJar is used to insert relevant cookies into every
// outbound Request and is updated with the cookie values
// of every inbound Response. The CookieJar is also consulted for every
// redirect that the Client follows.
//
// If CookieJar is nil, cookies are only sent if they are explicitly set on the Request.
CookieJar http.CookieJar
// Query string that will be added to the request URI.
// This will override the query string in URI.
SearchParams urlValues.Values
// Duration to wait for the server to end the response before aborting the request.
Timeout time.Duration
// Defines if redirect responses should be followed automatically.
FollowRedirect bool
// Additional configuration Options for FollowRedirect.
RedirectOptions RedirectOptions
// List of URls that have responded with a redirect so far.
redirectUrls []*url.URL
// Hooks allow modifications during the request lifecycle.
Hooks Hooks
}
type RetryOptions struct {
// Max number of times to retry.
Limit int
// Only retry when the request HTTP method equals one of these Methods.
Methods []string
// Only retry when the response HTTP status code equals one of these StatusCodes.
StatusCodes []int
// Only retry on error when the error message contains one of these ErrorCodes.
ErrorCodes []string
// Respect the response 'Retry-After' header, if set.
//
// If RetryAfter is false or the response headers don't contain this header,
// it will default to 0. You can specify a custom timeout with CalculateTimeout.
//
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
RetryAfter bool
// CalculateTimeout is a function that computes the timeout to use between retries.
// By default, `computedTimeout` will be used as timeout value.
CalculateTimeout func(retries int, retryOptions *RetryOptions, computedTimeout time.Duration, error error) time.Duration
}
func NewDefaultOptions() *Options {
jar, _ := cookiejar.New(&cookiejar.Options{})
return &Options{
URI: "",
Retry: true,
RetryOptions: NewDefaultRetryOptions(),
Method: http.MethodGet,
PrefixURL: "",
Headers: make(http.Header),
Body: nil,
Json: nil,
Form: nil,
UnmarshalJson: func(data []byte) (JSON, error) {
var result JSON
if err := json.Unmarshal(data, &result); err != nil {
return nil, err
}
return result, nil
},
MarshalJson: func(data JSON) ([]byte, error) {
result, err := json.Marshal(data)
if err != nil {
return nil, err
}
return result, nil
},
Context: nil,
CookieJar: jar,
SearchParams: nil,
Timeout: time.Second * 10,
FollowRedirect: true,
RedirectOptions: RedirectOptions{
Limit: 0,
RewriteMethods: true,
},
Hooks: Hooks{},
Adapter: &RequestAdapter{},
}
}
func NewDefaultRetryOptions() *RetryOptions {
return &RetryOptions{
Limit: 2,
Methods: []string{http.MethodGet, http.MethodPut, http.MethodHead, http.MethodDelete, http.MethodOptions, http.MethodTrace},
StatusCodes: []int{408, 413, 429, 500, 502, 503, 504, 521, 522, 524},
ErrorCodes: []string{"ETIMEDOUT", "ECONNRESET", "EADDRINUSE", "ECONNREFUSED", "EPIPE", "ENOTFOUND", "ENETUNREACH", "EAI_AGAIN"},
RetryAfter: true,
CalculateTimeout: func(retries int, retryOptions *RetryOptions, computedTimeout time.Duration, error error) time.Duration {
return computedTimeout
},
}
}
// Extend extends the current Options by the provided Options.
// The value returned is a pointer to a newly allocated Options value.
func (o *Options) Extend(options *Options) (*Options, error) {
// Create new copies of source and dest.
dst := *options
src := *o
// Exclude Adapter from being merged
src.Adapter = nil
if err := mergo.Merge(&dst, src); err != nil {
return nil, err
}
// Set the adapter again, if specified in either the parent or provided config.
if options.Adapter != nil {
dst.Adapter = options.Adapter
} else if o.Adapter != nil {
dst.Adapter = o.Adapter
}
// Mergo doesn't have an option to override bool zero values *only*, so we'll just do it ourselves.
if dst.Retry && !options.Retry {
dst.Retry = false
}
if dst.FollowRedirect && !options.FollowRedirect {
dst.FollowRedirect = false
}
if dst.RedirectOptions.RewriteMethods && !options.RedirectOptions.RewriteMethods {
dst.RedirectOptions.RewriteMethods = false
}
if options.RetryOptions != nil && dst.RetryOptions.RetryAfter && !options.RetryOptions.RetryAfter {
if dst.RetryOptions == nil {
dst.RetryOptions = options.RetryOptions
} else {
dst.RetryOptions.RetryAfter = false
}
}
return &dst, nil
}