-
Notifications
You must be signed in to change notification settings - Fork 0
/
request.go
230 lines (196 loc) · 6.7 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
package http
import (
"bytes"
"context"
"io"
"net/http"
"net/http/httputil"
)
// Request wraps the standard http.Request struct and adds fields for tracking
// request metrics and custom authentication details.
//
// NOTE: Request is not threadsafe. A request cannot be used by multiple goroutines
// concurrently.
type Request struct {
// Embedded standard http.Request. This makes a *Request act exactly
// like an *http.Request so that all meta methods are supported.
*http.Request
Metrics Metrics // Tracks various metrics related to request handling
}
// WithContext creates a new Request with the provided context. This allows you
// to pass metadata, deadlines, or cancellation signals throughout the request lifecycle.
//
// Parameters:
// - ctx: The new context to associate with the request.
//
// Returns:
// - req: A new Request with the updated context.
func (r *Request) WithContext(ctx context.Context) (req *Request) {
req = r
req.Request = req.Request.WithContext(ctx)
return
}
// BodyBytes reads the request body and returns it as a byte slice.
//
// Parameters: None.
//
// Returns:
// - body: The body content as a byte slice, or an empty slice if the body is nil.
// - err: An error if the body reading fails.
func (r *Request) BodyBytes() (body []byte, err error) {
if r.Request.Body == nil {
return
}
buf := new(bytes.Buffer)
_, err = buf.ReadFrom(r.Body)
if err != nil {
return
}
body = buf.Bytes()
return
}
// Clone creates a deep copy of the Request, resetting its Metrics and duplicating
// the Auth data, if available. This is useful for generating a new request instance
// while retaining most properties.
//
// Parameters:
// - ctx: The context to associate with the cloned request.
//
// Returns:
// - req: A new Request with the same data but reset Metrics and context.
func (r *Request) Clone(ctx context.Context) (req *Request) {
req = &Request{
Request: r.Request.Clone(ctx),
Metrics: Metrics{},
}
return
}
// Dump serializes the Request into a byte slice. Optionally includes the body
// in the dump if it is present and non-empty.
//
// Parameters: None.
//
// Returns:
// - dump: A byte slice representation of the request, including headers and optionally the body.
// - err: An error if dumping the request fails.
func (r *Request) Dump() (dump []byte, err error) {
resplen := int64(0)
dumpbody := true
clone := r.Clone(context.TODO())
if clone.Body != nil {
resplen, _ = getReaderLength(clone.Body)
}
if resplen == 0 {
dumpbody = false
clone.ContentLength = 0
clone.Body = nil
delete(clone.Header, "Content-length")
} else {
clone.ContentLength = resplen
}
dump, err = httputil.DumpRequestOut(clone.Request, dumpbody)
if err != nil {
return
}
return
}
// Metrics represents statistics related to request handling. These metrics are
// useful for tracking performance or issues encountered during the request lifecycle.
type Metrics struct {
Failures int // Failures is the number of failed requests
Retries int // Retries is the number of retries for the request
DrainErrors int // DrainErrors is number of errors occurred in draining response body
}
// NewRequest creates a new Request without context using the specified HTTP method, URL, and body.
//
// Parameters:
// - method: The HTTP method to use (e.g., GET, POST).
// - url: The URL to send the request to.
// - body: The request body, which can be nil.
//
// Returns:
// - req: A new Request object.
// - err: An error if request creation fails.
func NewRequest(method, url string, body interface{}) (req *Request, err error) {
req, err = NewRequestFromURL(url, method, body)
return
}
// NewRequestWithContext creates a new Request with the specified context.
//
// Parameters:
// - ctx: The context to associate with the request.
// - method: The HTTP method to use (e.g., GET, POST).
// - url: The URL to send the request to.
// - body: The request body, which can be nil.
//
// Returns:
// - req: A new Request object with the provided context.
// - err: An error if request creation fails.
func NewRequestWithContext(ctx context.Context, method, url string, body interface{}) (req *Request, err error) {
req, err = NewRequestFromURLWithContext(ctx, url, method, body)
return
}
// NewRequestFromURL creates a new Request without a context.
//
// Parameters:
// - url: The URL to send the request to.
// - method: The HTTP method to use.
// - body: The request body, which can be nil.
//
// Returns:
// - req: A new Request object with the default context (context.Background()).
// - err: An error if request creation fails.
func NewRequestFromURL(url, method string, body interface{}) (req *Request, err error) {
req, err = NewRequestFromURLWithContext(context.Background(), url, method, body)
return
}
// NewRequestFromURLWithContext creates a new Request with the specified context and body.
// It also calculates the content length if a body is provided and sets the appropriate headers.
//
// Parameters:
// - ctx: The context to associate with the request.
// - url: The URL to send the request to.
// - method: The HTTP method to use.
// - body: The request body, which can be nil.
//
// Returns:
// - req: A new Request object with the provided context.
// - err: An error if request creation fails.
func NewRequestFromURLWithContext(ctx context.Context, url, method string, body interface{}) (req *Request, err error) {
reqBodyReader, reqContentLength, err := getReusableBodyandContentLength(body)
if err != nil {
return
}
// we provide a url without path to http.NewRequest at start and then replace url instance directly
// because `http.NewRequest()` internally parses using `url.Parse()` this removes/overrides any
// patches done by urlutil.URL in unsafe mode (ex: https://scanme.sh/%invalid)
// Note: this does not have any impact on actual path when sending request
// `http.NewRequestxxx` internally only uses `u.Host` and all other data is stored in `url.URL` instance
httpReq, err := http.NewRequestWithContext(ctx, method, url, nil) //nolint:gocritic // To be refactored
if err != nil {
return
}
// content-length and body should be assigned only
// if request has body
if reqBodyReader != nil {
httpReq.ContentLength = reqContentLength
httpReq.Body = reqBodyReader
}
req = &Request{
Request: httpReq,
Metrics: Metrics{},
}
return
}
// getReaderLength reads the entire content of an io.Reader and returns its length. The data is discarded.
//
// Parameters:
// - reader: The io.Reader containing the data.
//
// Returns:
// - length: The number of bytes read from the reader.
// - err: An error if reading fails.
func getReaderLength(reader io.Reader) (length int64, err error) {
length, err = io.Copy(io.Discard, reader)
return
}