Skip to content

Commit 20d595a

Browse files
authored
Merge pull request #97 from deploymenttheory/dev
Refactor retry logic in executeRequestWithRetries function
2 parents 590c927 + 3edd774 commit 20d595a

File tree

2 files changed

+135
-28
lines changed

2 files changed

+135
-28
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// httpclient_error_response.go
2+
// This package provides utility functions and structures for handling and categorizing HTTP error responses.
3+
package httpclient
4+
5+
import (
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
11+
"github.com/deploymenttheory/go-api-http-client/logger"
12+
"go.uber.org/zap"
13+
)
14+
15+
// APIError represents a more flexible structure for API error responses.
16+
type APIError struct {
17+
StatusCode int // HTTP status code
18+
Type string // A brief identifier for the type of error
19+
Message string // Human-readable message
20+
Detail string // Detailed error message
21+
Errors map[string]interface{} // A map to hold various error fields
22+
Raw string // Raw response body for unstructured errors
23+
}
24+
25+
// StructuredError represents a structured error response from the API.
26+
type StructuredError struct {
27+
Error struct {
28+
Code string `json:"code"`
29+
Message string `json:"message"`
30+
} `json:"error"`
31+
}
32+
33+
// Error returns a string representation of the APIError.
34+
func (e *APIError) Error() string {
35+
return fmt.Sprintf("API Error (Type: %s, Code: %d): %s", e.Type, e.StatusCode, e.Message)
36+
}
37+
38+
// handleAPIErrorResponse attempts to parse the error response from the API and logs using zap logger.
39+
func handleAPIErrorResponse(resp *http.Response, log logger.Logger) *APIError {
40+
apiError := &APIError{StatusCode: resp.StatusCode}
41+
42+
// Attempt to parse the response into a StructuredError
43+
var structuredErr StructuredError
44+
if err := json.NewDecoder(resp.Body).Decode(&structuredErr); err == nil && structuredErr.Error.Message != "" {
45+
apiError.Type = structuredErr.Error.Code
46+
apiError.Message = structuredErr.Error.Message
47+
48+
// Log the structured error details with zap logger
49+
log.Warn("API returned structured error",
50+
zap.String("error_code", structuredErr.Error.Code),
51+
zap.String("error_message", structuredErr.Error.Message),
52+
zap.Int("status_code", resp.StatusCode),
53+
)
54+
55+
return apiError
56+
}
57+
58+
// If the structured error parsing fails, attempt a more generic parsing
59+
bodyBytes, err := io.ReadAll(resp.Body)
60+
if err != nil {
61+
// If reading the response body fails, store the error message and log the error
62+
apiError.Raw = "Failed to read API error response body"
63+
apiError.Message = err.Error()
64+
apiError.Type = "ReadError"
65+
66+
log.Error("Failed to read API error response body",
67+
zap.Error(err),
68+
)
69+
70+
return apiError
71+
}
72+
73+
if err := json.Unmarshal(bodyBytes, &apiError.Errors); err != nil {
74+
// If generic parsing also fails, store the raw response body and log the error
75+
apiError.Raw = string(bodyBytes)
76+
apiError.Message = "Failed to parse API error response"
77+
apiError.Type = "UnexpectedError"
78+
79+
log.Error("Failed to parse API error response",
80+
zap.String("raw_response", apiError.Raw),
81+
)
82+
83+
return apiError
84+
}
85+
86+
// Extract fields from the generic error map and log the error with extracted details
87+
if msg, ok := apiError.Errors["message"].(string); ok {
88+
apiError.Message = msg
89+
}
90+
if detail, ok := apiError.Errors["detail"].(string); ok {
91+
apiError.Detail = detail
92+
}
93+
94+
log.Error("API error",
95+
zap.Int("status_code", apiError.StatusCode),
96+
zap.String("type", apiError.Type),
97+
zap.String("message", apiError.Message),
98+
zap.String("detail", apiError.Detail),
99+
)
100+
101+
return apiError
102+
}

httpclient/httpclient_request.go

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -153,66 +153,71 @@ func (c *Client) executeRequestWithRetries(method, endpoint string, body, out in
153153
headerManager.SetRequestHeaders(endpoint)
154154
headerManager.LogHeaders(c)
155155

156-
// Calculate the deadline for all retry attempts based on the client configuration.
156+
// Define a retry deadline based on the client's total retry duration configuration
157157
totalRetryDeadline := time.Now().Add(c.clientConfig.ClientOptions.TotalRetryDuration)
158158

159-
// Execute the HTTP request with retries
160159
var resp *http.Response
161160
var retryCount int
162-
var requestStartTime = time.Now()
163-
164-
for time.Now().Before(totalRetryDeadline) {
161+
for time.Now().Before(totalRetryDeadline) { // Check if the current time is before the total retry deadline
162+
req = req.WithContext(ctx)
165163
resp, err = c.do(req, log, method, endpoint)
166-
167-
// Successful response handling
168-
if err == nil && resp.StatusCode >= 200 && resp.StatusCode < 300 {
169-
log.LogRequestEnd(method, endpoint, resp.StatusCode, time.Since(requestStartTime))
164+
// Check for successful status code
165+
if err == nil && resp.StatusCode >= 200 && resp.StatusCode < 400 {
166+
if resp.StatusCode >= 300 {
167+
log.Warn("Redirect response received", zap.Int("status_code", resp.StatusCode), zap.String("location", resp.Header.Get("Location")))
168+
}
169+
// Handle the response as successful, even if it's a redirect.
170170
return resp, c.handleSuccessResponse(resp, out, log, method, endpoint)
171171
}
172172

173-
// Log and handle non-retryable errors immediately without retrying
173+
// Leverage TranslateStatusCode for more descriptive error logging
174+
statusMessage := status.TranslateStatusCode(resp)
175+
176+
// Check for non-retryable errors
174177
if resp != nil && status.IsNonRetryableStatusCode(resp) {
175-
log.LogError(method, endpoint, resp.StatusCode, err, status.TranslateStatusCode(resp))
176-
return resp, err
178+
log.Warn("Non-retryable error received", zap.Int("status_code", resp.StatusCode), zap.String("status_message", statusMessage))
179+
return resp, handleAPIErrorResponse(resp, log)
177180
}
178181

179-
// Handle rate-limiting errors by parsing the 'Retry-After' header and waiting before the next retry
182+
// Parsing rate limit headers if a rate-limit error is detected
180183
if status.IsRateLimitError(resp) {
181184
waitDuration := parseRateLimitHeaders(resp, log)
182-
log.LogRateLimiting(method, endpoint, resp.Header.Get("Retry-After"), waitDuration)
183-
time.Sleep(waitDuration)
184-
continue
185+
if waitDuration > 0 {
186+
log.Warn("Rate limit encountered, waiting before retrying", zap.Duration("waitDuration", waitDuration))
187+
time.Sleep(waitDuration)
188+
continue // Continue to next iteration after waiting
189+
}
185190
}
186191

187-
// Retry the request for transient errors using exponential backoff with jitter
192+
// Handling retryable errors with exponential backoff
188193
if status.IsTransientError(resp) {
189194
retryCount++
190195
if retryCount > c.clientConfig.ClientOptions.MaxRetryAttempts {
191-
// Log max retry attempts reached with structured logging
192-
log.LogError(method, endpoint, resp.StatusCode, err, "Max retry attempts reached")
193-
break
196+
log.Warn("Max retry attempts reached", zap.String("method", method), zap.String("endpoint", endpoint))
197+
break // Stop retrying if max attempts are reached
194198
}
195199
waitDuration := calculateBackoff(retryCount)
196-
log.LogRetryAttempt(method, endpoint, retryCount, "Transient error", waitDuration, err)
197-
time.Sleep(waitDuration)
198-
continue
200+
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))
201+
time.Sleep(waitDuration) // Wait before retrying
202+
continue // Continue to next iteration after waiting
199203
}
200204

201-
// Log non-retryable API errors and break the retry loop
205+
// Handle error responses
202206
if err != nil || !status.IsRetryableStatusCode(resp.StatusCode) {
207+
if apiErr := handleAPIErrorResponse(resp, log); apiErr != nil {
208+
err = apiErr
209+
}
203210
log.LogError(method, endpoint, resp.StatusCode, err, status.TranslateStatusCode(resp))
204211
break
205212
}
206213
}
207214

208-
// Final error handling after all retries are exhausted
215+
// Handles final non-API error.
209216
if err != nil {
210-
// Log the final error after retries with structured logging
211-
log.LogError(method, endpoint, 0, err, "Final error after retries")
212217
return nil, err
213218
}
214219

215-
return resp, nil
220+
return resp, handleAPIErrorResponse(resp, log)
216221
}
217222

218223
// executeRequest executes an HTTP request using the specified method, endpoint, and request body without implementing

0 commit comments

Comments
 (0)