-
Notifications
You must be signed in to change notification settings - Fork 93
Expand file tree
/
Copy pathrequest.go
More file actions
353 lines (302 loc) · 10.5 KB
/
Copy pathrequest.go
File metadata and controls
353 lines (302 loc) · 10.5 KB
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
package surf
import (
"bytes"
"context"
"fmt"
"io"
"net"
"time"
"github.com/enetx/g"
"github.com/enetx/http"
"github.com/enetx/surf/header"
"github.com/enetx/surf/internal/drainbody"
"github.com/enetx/surf/internal/retryafter"
)
// Request represents an HTTP request with additional surf-specific functionality.
// It wraps the standard http.Request and provides enhanced features like middleware support,
// retry capabilities, remote address tracking, and structured error handling.
type Request struct {
err error // General error associated with the request (validation, setup, etc.)
remoteAddr net.Addr // Remote server address captured during connection
bodyBytes []byte // Cached body bytes for retry support
request *http.Request // The underlying standard HTTP request
cli *Client // The associated surf client for this request
multipart *Multipart // Multipart form data for file uploads and form submissions
}
// GetRequest returns the underlying standard http.Request.
// Provides access to the wrapped HTTP request for advanced use cases.
func (req *Request) GetRequest() *http.Request { return req.request }
// Multipart sets multipart form data for the request.
// The provided Multipart object contains form fields and files to be sent.
// Returns the request for method chaining. If m is nil, an error is set on the request.
func (req *Request) Multipart(m *Multipart) *Request {
if req.err != nil {
return req
}
if m == nil {
req.err = fmt.Errorf("multipart is nil")
return req
}
req.multipart = m
return req
}
// prepareMultipart prepares the multipart body for the request.
// It sets up the request body with a pipe reader and configures the Content-Type header.
// Returns an error if both Body() and Multipart() were called, as they are mutually exclusive.
func (req *Request) prepareMultipart() {
if req.multipart == nil {
return
}
if req.request.Body != nil {
req.err = fmt.Errorf("cannot use both Body() and Multipart() - they are mutually exclusive")
return
}
pr, contentType, err := req.multipart.prepareWriter(req.cli.boundary)
if err != nil {
req.err = err
return
}
req.request.Body = pr
req.request.Header.Set(header.CONTENT_TYPE, contentType)
}
// Do executes the HTTP request and returns a Response wrapped in a Result type.
// This is the main method that performs the actual HTTP request with full surf functionality:
// - Applies request middleware (authentication, headers, tracing, etc.)
// - Preserves request body for potential retries
// - Implements retry logic with configurable status codes and delays
// - Measures request timing for performance analysis
// - Handles request preparation errors and write errors
func (req *Request) Do() g.Result[*Response] {
// Return early if request has preparation errors
if req.err != nil {
return g.Err[*Response](req.err)
}
req.prepareMultipart()
if req.err != nil {
return g.Err[*Response](req.err)
}
if err := req.cli.applyReqMW(req); err != nil {
return g.Err[*Response](err)
}
if req.request.Method != http.MethodHead {
if req.multipart == nil || req.multipart.retry {
req.bodyBytes, req.request.Body, req.err = drainbody.DrainBody(req.request.Body)
if req.err != nil {
return g.Err[*Response](req.err)
}
}
}
var (
resp *http.Response
attempts int
err error
)
start := time.Now()
cli := req.cli.cli
builder := req.cli.builder
retry:
// Restore body from saved bytes for retry attempts
if attempts > 0 && req.bodyBytes != nil {
req.request.Body = io.NopCloser(bytes.NewReader(req.bodyBytes))
}
resp, err = cli.Do(req.request)
if err != nil {
return g.Err[*Response](err)
}
// Check if retry is needed based on status code and retry configuration
if builder != nil && builder.retryMax != 0 && attempts < builder.retryMax && !builder.retryCodes.IsEmpty() &&
builder.retryCodes.Contains(resp.StatusCode) {
retryAfter, _ := retryafter.Parse(resp.Header.Get(header.RETRY_AFTER), time.Now())
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
attempts++
delay := max(builder.retryWait, retryAfter)
if delay > 0 {
timer := time.NewTimer(delay)
select {
case <-timer.C:
case <-req.request.Context().Done():
timer.Stop()
return g.Err[*Response](req.request.Context().Err())
}
} else if ctx := req.request.Context(); ctx.Err() != nil {
return g.Err[*Response](ctx.Err())
}
goto retry
}
response := &Response{
Attempts: attempts,
Time: time.Since(start),
Client: req.cli,
ContentLength: resp.ContentLength,
Cookies: resp.Cookies(),
Headers: Headers(resp.Header),
Proto: g.String(resp.Proto),
StatusCode: StatusCode(resp.StatusCode),
URL: resp.Request.URL,
UserAgent: g.String(req.request.UserAgent()),
remoteAddr: req.remoteAddr,
request: req,
response: resp,
}
if req.request.Method != http.MethodHead {
response.Body = &Body{
Reader: resp.Body,
cache: builder != nil && builder.cacheBody,
contentType: resp.Header.Get(header.CONTENT_TYPE),
contentLength: resp.ContentLength,
limit: -1,
ctx: req.request.Context(),
}
}
if err := req.cli.applyRespMW(response); err != nil {
return g.Err[*Response](err)
}
return g.Ok(response)
}
// WithContext associates a context with the request for cancellation and deadlines.
// The context can be used to cancel the request, set timeouts, or pass request-scoped values.
// Returns the request for method chaining. If ctx is nil, the request is unchanged.
func (req *Request) WithContext(ctx context.Context) *Request {
if ctx != nil {
req.request = req.request.WithContext(ctx)
}
return req
}
// AddCookies adds one or more HTTP cookies to the request.
// Cookies are added to the request headers and will be sent with the HTTP request.
// Returns the request for method chaining.
func (req *Request) AddCookies(cookies ...*http.Cookie) *Request {
for _, cookie := range cookies {
req.request.AddCookie(cookie)
}
return req
}
// SetHeaders sets HTTP headers for the request, replacing any existing headers with the same name.
// Supports multiple input formats:
// - Two arguments: key, value (string or g.String)
// - Single argument: http.Header, Headers, map types, or g.Map types
// Maintains header order for fingerprinting purposes when using g.MapOrd.
// Returns the request for method chaining.
func (req *Request) SetHeaders(headers ...any) *Request {
if req.request == nil || headers == nil {
return req
}
req.applyHeaders(headers, func(h http.Header, k, v string) { h.Set(k, v) })
return req
}
// AddHeaders adds HTTP headers to the request, appending to any existing headers with the same name.
// Unlike SetHeaders, this method preserves existing headers and adds new values.
// Supports the same input formats as SetHeaders.
// Returns the request for method chaining.
func (req *Request) AddHeaders(headers ...any) *Request {
if req.request == nil || headers == nil {
return req
}
req.applyHeaders(headers, func(h http.Header, k, v string) { h.Add(k, v) })
return req
}
// applyHeaders is a helper function that processes various header input formats and applies them to an HTTP request.
// It handles type checking, conversion, and delegation to the provided setOrAdd function for actual header manipulation.
// Supports ordered header maps for fingerprinting and maintains compatibility with multiple map and header types.
func (req *Request) applyHeaders(
rawHeaders []any,
setOrAdd func(h http.Header, key, value string),
) {
r := req.request
if len(rawHeaders) >= 2 {
var key, value string
switch k := rawHeaders[0].(type) {
case string:
key = k
case g.String:
key = k.Std()
default:
panic(fmt.Sprintf("unsupported key type: expected 'string' or 'String', got %T", rawHeaders[0]))
}
switch v := rawHeaders[1].(type) {
case string:
value = v
case g.String:
value = v.Std()
default:
panic(fmt.Sprintf("unsupported value type: expected 'string' or 'String', got %T", rawHeaders[1]))
}
setOrAdd(r.Header, key, value)
return
}
switch h := rawHeaders[0].(type) {
case http.Header:
for key, values := range h {
for _, value := range values {
setOrAdd(r.Header, key, value)
}
}
case Headers:
for key, values := range h {
for _, value := range values {
setOrAdd(r.Header, key, value)
}
}
case map[string]string:
for key, value := range h {
setOrAdd(r.Header, key, value)
}
case map[g.String]g.String:
for key, value := range h {
setOrAdd(r.Header, key.Std(), value.Std())
}
case g.Map[string, string]:
for key, value := range h {
setOrAdd(r.Header, key, value)
}
case g.Map[g.String, g.String]:
for key, value := range h {
setOrAdd(r.Header, key.Std(), value.Std())
}
case g.MapOrd[string, string]:
updated := updateRequestHeaderOrder(req, h)
updated.Iter().ForEach(func(key, value string) { setOrAdd(r.Header, key, value) })
case g.MapOrd[g.String, g.String]:
updated := updateRequestHeaderOrder(req, h)
updated.Iter().ForEach(func(key, value g.String) { setOrAdd(r.Header, key.Std(), value.Std()) })
default:
panic(
fmt.Sprintf(
"unsupported headers type: expected 'http.Header', 'surf.Headers', 'map[~string]~string', 'Map[~string, ~string]', or 'MapOrd[~string, ~string]', got %T",
rawHeaders[0],
),
)
}
}
// updateRequestHeaderOrder processes ordered headers for HTTP/2 and HTTP/3 fingerprinting.
// It maintains the specific order of headers which is crucial for browser fingerprinting.
// Separates regular headers from pseudo-headers (starting with ':') and sets the appropriate
// header order keys for the transport layer to use. Returns a filtered map containing only
// non-pseudo headers with non-empty values.
func updateRequestHeaderOrder[T ~string](r *Request, h g.MapOrd[T, T]) g.MapOrd[T, T] {
h = h.Clone()
if r.cli.builder != nil {
method := r.request.Method
if r.cli.builder.forceHTTP3 {
method += "http3"
}
if r.cli.builder.headersApplier != nil {
r.cli.builder.headersApplier(&h, method)
}
}
headersKeys := h.Iter().
Keys().
Map(func(s T) string { return string(g.String(s).Lower()) }).
Collect()
headers, pheaders := headersKeys.Iter().Partition(func(v string) bool { return v[0] != ':' })
if !headers.IsEmpty() {
r.request.Header[http.HeaderOrderKey] = headers
}
if !pheaders.IsEmpty() {
r.request.Header[http.PHeaderOrderKey] = pheaders
}
return h.Iter().
Filter(func(header, data T) bool { return header[0] != ':' && len(data) != 0 }).
Collect()
}