generated from deploymenttheory/Template
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhttpclient_request.go
517 lines (451 loc) · 22.3 KB
/
httpclient_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
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
// http_request.go
package httpclient
import (
"bytes"
"context"
"net/http"
"time"
"github.com/deploymenttheory/go-api-http-client/logger"
"github.com/deploymenttheory/go-api-http-client/status"
"github.com/google/uuid"
"go.uber.org/zap"
)
// DoRequest constructs and executes an HTTP request based on the provided method, endpoint, request body, and output variable.
// This function serves as a dispatcher, deciding whether to execute the request with or without retry logic based on the
// idempotency of the HTTP method. Idempotent methods (GET, PUT, DELETE) are executed with retries to handle transient errors
// and rate limits, while non-idempotent methods (POST, PATCH) are executed without retries to avoid potential side effects
// of duplicating non-idempotent operations. function uses an instance of a logger implementing the logger.Logger interface, used to log informational messages, warnings, and
// errors encountered during the execution of the request.
// Parameters:
// - method: A string representing the HTTP method to be used for the request. This method determines the execution path
// and whether the request will be retried in case of failures.
// - endpoint: The target API endpoint for the request. This should be a relative path that will be appended to the base URL
// configured for the HTTP client.
// - body: The payload for the request, which will be serialized into the request body. The serialization format (e.g., JSON, XML)
// is determined by the content-type header and the specific implementation of the API handler used by the client.
// - out: A pointer to an output variable where the response will be deserialized. The function expects this to be a pointer to
// a struct that matches the expected response schema.
// Returns:
// - *http.Response: The HTTP response received from the server. In case of successful execution, this response contains
// the status code, headers, and body of the response. In case of errors, particularly after exhausting retries for
// idempotent methods, this response may contain the last received HTTP response that led to the failure.
// - error: An error object indicating failure during request execution. This could be due to network issues, server errors,
// or a failure in request serialization/deserialization. For idempotent methods, an error is returned if all retries are
// exhausted without success.
// Usage:
// This function is the primary entry point for executing HTTP requests using the client. It abstracts away the details of
// request retries, serialization, and response handling, providing a simplified interface for making HTTP requests. It is
// suitable for a wide range of HTTP operations, from fetching data with GET requests to submitting data with POST requests.
// Example:
// var result MyResponseType
// resp, err := client.DoRequest("GET", "/api/resource", nil, &result, logger)
// if err != nil {
// // Handle error
// }
// // Use `result` or `resp` as needed
// Note:
// - The caller is responsible for closing the response body when not nil to avoid resource leaks.
// - The function ensures concurrency control by managing concurrency tokens internally, providing safe concurrent operations
// within the client's concurrency model.
// - The decision to retry requests is based on the idempotency of the HTTP method and the client's retry configuration,
// including maximum retry attempts and total retry duration.
func (c *Client) DoRequest(method, endpoint string, body, out interface{}) (*http.Response, error) {
log := c.Logger
if IsIdempotentHTTPMethod(method) {
return c.executeRequestWithRetries(method, endpoint, body, out)
} else if IsNonIdempotentHTTPMethod(method) {
return c.executeRequest(method, endpoint, body, out)
} else {
return nil, log.Error("HTTP method not supported", zap.String("method", method))
}
}
// executeRequestWithRetries executes an HTTP request using the specified method, endpoint, request body, and output variable.
// It is designed for idempotent HTTP methods (GET, PUT, DELETE), where the request can be safely retried in case of
// transient errors or rate limiting. The function implements a retry mechanism that respects the client's configuration
// for maximum retry attempts and total retry duration. Each retry attempt uses exponential backoff with jitter to avoid
// thundering herd problems. An instance of a logger (conforming to the logger.Logger interface) is used for logging the
// request, retry attempts, and any errors encountered.
//
// Parameters:
// - method: The HTTP method to be used for the request (e.g., "GET", "PUT", "DELETE").
// - endpoint: The API endpoint to which the request will be sent. This should be a relative path that will be appended
// to the base URL of the HTTP client.
// - body: The request payload, which will be marshaled into the request body based on the content type. Can be nil for
// methods that do not send a payload.
// - out: A pointer to the variable where the unmarshaled response will be stored. The function expects this to be a
// pointer to a struct that matches the expected response schema.
// - log:
//
// Returns:
// - *http.Response: The HTTP response from the server, which may be the response from a successful request or the last
// failed attempt if all retries are exhausted.
// - error: An error object if an error occurred during the request execution or if all retry attempts failed. The error
// may be a structured API error parsed from the response or a generic error indicating the failure reason.
//
// Usage:
// This function should be used for operations that are safe to retry and where the client can tolerate the additional
// latency introduced by the retry mechanism. It is particularly useful for handling transient errors and rate limiting
// responses from the server.
//
// Note:
// - The caller is responsible for closing the response body to prevent resource leaks.
// - The function respects the client's concurrency token, acquiring and releasing it as needed to ensure safe concurrent
// operations.
// - The retry mechanism employs exponential backoff with jitter to mitigate the impact of retries on the server.
func (c *Client) executeRequestWithRetries(method, endpoint string, body, out interface{}) (*http.Response, error) {
log := c.Logger
// Include the core logic for handling non-idempotent requests with retries here.
log.Debug("Executing request with retries", zap.String("method", method), zap.String("endpoint", endpoint))
// Auth Token validation check
valid, err := c.ValidAuthTokenCheck()
if err != nil || !valid {
return nil, err
}
// Acquire a token for concurrency management
ctx, err := c.AcquireConcurrencyToken(context.Background())
if err != nil {
return nil, err
}
defer func() {
// Extract the requestID from the context and release the concurrency token
if requestID, ok := ctx.Value(requestIDKey{}).(uuid.UUID); ok {
c.ConcurrencyMgr.Release(requestID)
}
}()
// Marshal Request with correct encoding defined in api handler
requestData, err := c.APIHandler.MarshalRequest(body, method, endpoint, log)
if err != nil {
return nil, err
}
// Construct URL with correct structure defined in api handler
url := c.APIHandler.ConstructAPIResourceEndpoint(c.InstanceName, endpoint, log)
// Initialize total request counter
c.PerfMetrics.lock.Lock()
c.PerfMetrics.TotalRequests++
c.PerfMetrics.lock.Unlock()
// Perform Request
req, err := http.NewRequest(method, url, bytes.NewBuffer(requestData))
if err != nil {
return nil, err
}
// Set request headers
headerManager := NewHeaderManager(req, log, c.APIHandler, c.Token)
headerManager.SetRequestHeaders(endpoint)
headerManager.LogHeaders(c)
// Define a retry deadline based on the client's total retry duration configuration
totalRetryDeadline := time.Now().Add(c.clientConfig.ClientOptions.TotalRetryDuration)
var resp *http.Response
var retryCount int
for time.Now().Before(totalRetryDeadline) { // Check if the current time is before the total retry deadline
req = req.WithContext(ctx)
// Log outgoing cookies
log.LogCookies("outgoing", req, method, endpoint)
// Execute the HTTP request
resp, err = c.do(req, log, method, endpoint)
// Log outgoing cookies
log.LogCookies("incoming", req, method, endpoint)
// Check for successful status code
if err == nil && resp.StatusCode >= 200 && resp.StatusCode < 400 {
if resp.StatusCode >= 300 {
log.Warn("Redirect response received", zap.Int("status_code", resp.StatusCode), zap.String("location", resp.Header.Get("Location")))
}
// Handle the response as successful, even if it's a redirect.
return resp, c.handleSuccessResponse(resp, out, log, method, endpoint)
}
// Leverage TranslateStatusCode for more descriptive error logging
statusMessage := status.TranslateStatusCode(resp)
// Check for non-retryable errors
if resp != nil && status.IsNonRetryableStatusCode(resp) {
log.Warn("Non-retryable error received", zap.Int("status_code", resp.StatusCode), zap.String("status_message", statusMessage))
return resp, handleAPIErrorResponse(resp, log)
}
// Parsing rate limit headers if a rate-limit error is detected
if status.IsRateLimitError(resp) {
waitDuration := parseRateLimitHeaders(resp, log)
if waitDuration > 0 {
log.Warn("Rate limit encountered, waiting before retrying", zap.Duration("waitDuration", waitDuration))
time.Sleep(waitDuration)
continue // Continue to next iteration after waiting
}
}
// Handling retryable errors with exponential backoff
if status.IsTransientError(resp) {
retryCount++
if retryCount > c.clientConfig.ClientOptions.MaxRetryAttempts {
log.Warn("Max retry attempts reached", zap.String("method", method), zap.String("endpoint", endpoint))
break // Stop retrying if max attempts are reached
}
waitDuration := calculateBackoff(retryCount)
log.Warn("Retrying request due to transient error", zap.String("method", method), zap.String("endpoint", endpoint), zap.Int("retryCount", retryCount), zap.Duration("waitDuration", waitDuration), zap.Error(err))
time.Sleep(waitDuration) // Wait before retrying
continue // Continue to next iteration after waiting
}
// Handle error responses
if err != nil || !status.IsRetryableStatusCode(resp.StatusCode) {
if apiErr := handleAPIErrorResponse(resp, log); apiErr != nil {
err = apiErr
}
log.LogError("request_error", method, endpoint, resp.StatusCode, resp.Status, err, status.TranslateStatusCode(resp))
break
}
}
// Handles final non-API error.
if err != nil {
return nil, err
}
return resp, handleAPIErrorResponse(resp, log)
}
// executeRequest executes an HTTP request using the specified method, endpoint, and request body without implementing
// retry logic. It is primarily designed for non idempotent HTTP methods like POST and PATCH, where the request should
// not be automatically retried within this function due to the potential side effects of re-submitting the same data.
//
// Parameters:
// - method: The HTTP method to be used for the request, typically "POST" or "PATCH".
// - endpoint: The API endpoint to which the request will be sent. This should be a relative path that will be appended
// to the base URL of the HTTP client.
// - body: The request payload, which will be marshaled into the request body based on the content type. This can be any
// data structure that can be marshaled into the expected request format (e.g., JSON, XML).
// - out: A pointer to the variable where the unmarshaled response will be stored. This should be a pointer to a struct
//
// that matches the expected response schema.
// - log: An instance of a logger (conforming to the logger.Logger interface) used for logging the request and any errors
// encountered.
//
// Returns:
// - *http.Response: The HTTP response from the server. This includes the status code, headers, and body of the response.
// - error: An error object if an error occurred during the request execution. This could be due to network issues,
// server errors, or issues with marshaling/unmarshaling the request/response.
//
// Usage:
// This function is suitable for operations where the request should not be retried automatically, such as data submission
// operations where retrying could result in duplicate data processing. It ensures that the request is executed exactly
// once and provides detailed logging for debugging purposes.
//
// Note:
// - The caller is responsible for closing the response body to prevent resource leaks.
// - The function ensures concurrency control by acquiring and releasing a concurrency token before and after the request
// execution.
// - The function logs detailed information about the request execution, including the method, endpoint, status code, and
// any errors encountered.
func (c *Client) executeRequest(method, endpoint string, body, out interface{}) (*http.Response, error) {
log := c.Logger
// Include the core logic for handling idempotent requests here.
log.Debug("Executing request without retries", zap.String("method", method), zap.String("endpoint", endpoint))
// Auth Token validation check
valid, err := c.ValidAuthTokenCheck()
if err != nil || !valid {
return nil, err
}
// Acquire a token for concurrency management
ctx, err := c.AcquireConcurrencyToken(context.Background())
if err != nil {
return nil, err
}
defer func() {
// Extract the requestID from the context and release the concurrency token
if requestID, ok := ctx.Value(requestIDKey{}).(uuid.UUID); ok {
c.ConcurrencyMgr.Release(requestID)
}
}()
// Determine which set of encoding and content-type request rules to use
apiHandler := c.APIHandler
// Marshal Request with correct encoding
requestData, err := apiHandler.MarshalRequest(body, method, endpoint, log)
if err != nil {
return nil, err
}
// Construct URL using the ConstructAPIResourceEndpoint function
url := c.APIHandler.ConstructAPIResourceEndpoint(c.InstanceName, endpoint, log)
// Perform Request
req, err := http.NewRequest(method, url, bytes.NewBuffer(requestData))
if err != nil {
return nil, err
}
// Set request headers
headerManager := NewHeaderManager(req, log, c.APIHandler, c.Token)
headerManager.SetRequestHeaders(endpoint)
headerManager.LogHeaders(c)
req = req.WithContext(ctx)
// Log outgoing cookies
log.LogCookies("outgoing", req, method, endpoint)
// Execute the HTTP request
resp, err := c.do(req, log, method, endpoint)
if err != nil {
return nil, err
}
// Log incoming cookies
log.LogCookies("incoming", req, method, endpoint)
// Checks for the presence of a deprecation header in the HTTP response and logs if found.
CheckDeprecationHeader(resp, log)
// Check for successful status code, including redirects
if resp.StatusCode >= 200 && resp.StatusCode < 400 {
// Warn on redirects but proceed as successful
if resp.StatusCode >= 300 {
log.Warn("Redirect response received", zap.Int("status_code", resp.StatusCode), zap.String("location", resp.Header.Get("Location")))
}
return resp, c.handleSuccessResponse(resp, out, log, method, endpoint)
}
// Handle error responses for status codes outside the successful range
return nil, c.handleErrorResponse(resp, out, log, method, endpoint)
}
// do sends an HTTP request using the client's HTTP client. It logs the request and error details, if any,
// using structured logging with zap fields.
//
// Parameters:
// - req: The *http.Request object that contains all the details of the HTTP request to be sent.
// - log: An instance of a logger (conforming to the logger.Logger interface) used for logging the request details and any
// errors.
// - method: The HTTP method used for the request, used for logging.
// - endpoint: The API endpoint the request is being sent to, used for logging.
//
// Returns:
// - *http.Response: The HTTP response from the server.
// - error: An error object if an error occurred while sending the request or nil if no error occurred.
//
// Usage:
// This function should be used whenever the client needs to send an HTTP request. It abstracts away the common logic of
// request execution and error handling, providing detailed logs for debugging and monitoring.
func (c *Client) do(req *http.Request, log logger.Logger, method, endpoint string) (*http.Response, error) {
resp, err := c.httpClient.Do(req)
if err != nil {
// Log the error with structured logging, including method, endpoint, and the error itself
log.Error("Failed to send request",
zap.String("method", method),
zap.String("endpoint", endpoint),
zap.Error(err),
)
return nil, err
}
// Log the response status code for successful requests
log.Debug("Request sent successfully",
zap.String("method", method),
zap.String("endpoint", endpoint),
zap.Int("status_code", resp.StatusCode),
)
return resp, nil
}
// handleErrorResponse processes and logs errors from an HTTP response, allowing for a customizable error message.
//
// Parameters:
// - resp: The *http.Response received from the server.
// - log: An instance of a logger (conforming to the logger.Logger interface) for logging the error details.
// - errorMessage: A custom error message that provides context about the error.
// - method: The HTTP method used for the request, for logging purposes.
// - endpoint: The endpoint the request was sent to, for logging purposes.
//
// Returns:
// - An error object parsed from the HTTP response, indicating the nature of the failure.
func (c *Client) handleErrorResponse(resp *http.Response, out interface{}, log logger.Logger, method, endpoint string) error {
if err := c.APIHandler.HandleAPIErrorResponse(resp, out, log); err != nil {
log.Error("Failed to unmarshal HTTP response",
zap.String("method", method),
zap.String("endpoint", endpoint),
zap.Error(err),
)
return err
}
log.Info("HTTP request succeeded",
zap.String("method", method),
zap.String("endpoint", endpoint),
zap.Int("status_code", resp.StatusCode),
)
return nil
}
// handleSuccessResponse unmarshals a successful HTTP response into the provided output parameter and logs the
// success details. It's designed for use when the response indicates success (status code within 200-299).
// The function logs the request's success and, in case of unmarshalling errors, logs the failure and returns the error.
//
// Parameters:
// - resp: The *http.Response received from the server.
// - out: A pointer to the variable where the unmarshalled response will be stored.
// - log: An instance of a logger (conforming to the logger.Logger interface) for logging success or unmarshalling errors.
// - method: The HTTP method used for the request, for logging purposes.
// - endpoint: The endpoint the request was sent to, for logging purposes.
//
// Returns:
// - nil if the response was successfully unmarshalled into the 'out' parameter, or an error if unmarshalling failed.
func (c *Client) handleSuccessResponse(resp *http.Response, out interface{}, log logger.Logger, method, endpoint string) error {
if err := c.APIHandler.HandleAPISuccessResponse(resp, out, log); err != nil {
log.Error("Failed to unmarshal HTTP response",
zap.String("method", method),
zap.String("endpoint", endpoint),
zap.Error(err),
)
return err
}
log.Info("HTTP request succeeded",
zap.String("method", method),
zap.String("endpoint", endpoint),
zap.Int("status_code", resp.StatusCode),
)
return nil
}
// DoMultipartRequest creates and executes a multipart HTTP request. It is used for sending files
// and form fields in a single request. This method handles the construction of the multipart
// message body, setting the appropriate headers, and sending the request to the given endpoint.
//
// Parameters:
// - method: The HTTP method to use (e.g., POST, PUT).
// - endpoint: The API endpoint to which the request will be sent.
// - fields: A map of form fields and their values to include in the multipart message.
// - files: A map of file field names to file paths that will be included as file attachments.
// - out: A pointer to a variable where the unmarshaled response will be stored.
//
// Returns:
// - A pointer to the http.Response received from the server.
// - An error if the request could not be sent or the response could not be processed.
//
// The function first validates the authentication token, then constructs the multipart
// request body based on the provided fields and files. It then constructs the full URL for
// the request, sets the required headers (including Authorization and Content-Type), and
// sends the request.
//
// If debug mode is enabled, the function logs all the request headers before sending the request.
// After the request is sent, the function checks the response status code. If the response is
// not within the success range (200-299), it logs an error and returns the response and an error.
// If the response is successful, it attempts to unmarshal the response body into the 'out' parameter.
//
// Note:
// The caller should handle closing the response body when successful.
func (c *Client) DoMultipartRequest(method, endpoint string, fields map[string]string, files map[string]string, out interface{}) (*http.Response, error) {
log := c.Logger
// Auth Token validation check
valid, err := c.ValidAuthTokenCheck()
if err != nil || !valid {
return nil, err
}
// Determine which set of encoding and content-type request rules to use
//apiHandler := c.APIHandler
// Marshal the multipart form data
requestData, contentType, err := c.APIHandler.MarshalMultipartRequest(fields, files, log)
if err != nil {
return nil, err
}
// Construct URL using the ConstructAPIResourceEndpoint function
url := c.APIHandler.ConstructAPIResourceEndpoint(c.InstanceName, endpoint, log)
// Create the request
req, err := http.NewRequest(method, url, bytes.NewBuffer(requestData))
if err != nil {
return nil, err
}
// Initialize HeaderManager
headerManager := NewHeaderManager(req, log, c.APIHandler, c.Token)
// Use HeaderManager to set headers
headerManager.SetContentType(contentType)
headerManager.SetRequestHeaders(endpoint)
headerManager.LogHeaders(c)
// Execute the request
resp, err := c.do(req, log, method, endpoint)
if err != nil {
return nil, err
}
// Check for successful status code
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
// Handle error responses
//return nil, c.handleErrorResponse(resp, log, "Failed to process the HTTP request", method, endpoint)
return nil, c.handleErrorResponse(resp, out, log, method, endpoint)
} else {
// Handle successful responses
return resp, c.handleSuccessResponse(resp, out, log, method, endpoint)
}
}