fix: add retry + User-Agent to doRequest (P0.1 — DDoS-Guard bypass + transient error handling)
This commit is contained in:
@@ -802,42 +802,85 @@ func (c *UniversalClient) waitForInstanceIdle(ctx context.Context, instanceUid s
|
||||
// Internal HTTP helpers
|
||||
|
||||
func (c *UniversalClient) doRequest(ctx context.Context, method, path string, payload interface{}) ([]byte, http.Header, error) {
|
||||
var body io.Reader
|
||||
if payload != nil {
|
||||
b, err := json.Marshal(payload)
|
||||
// User-Agent: браузерный, чтобы пройти DDoS-Guard (см. docs/ops/API_TOKENS.md).
|
||||
// Go-http-client по умолчанию блокируется фильтром ddos-guard.
|
||||
const userAgent = "Mozilla/5.0 (compatible; Terraform-Provider-Nubes)"
|
||||
const maxRetries = 3
|
||||
baseDelay := 2 * time.Second
|
||||
|
||||
var lastErr error
|
||||
for attempt := 0; attempt <= maxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
delay := baseDelay * time.Duration(1<<(attempt-1)) // 2s, 4s, 8s
|
||||
select {
|
||||
case <-time.After(delay):
|
||||
case <-ctx.Done():
|
||||
return nil, nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
var body io.Reader
|
||||
if payload != nil {
|
||||
b, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
body = bytes.NewBuffer(b)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, c.ApiEndpoint+path, body)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
body = bytes.NewBuffer(b)
|
||||
|
||||
req.Close = true
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
if c.ApiToken != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.ApiToken)
|
||||
}
|
||||
|
||||
resp, err := c.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
// Сетевые ошибки — retry (кроме последней попытки)
|
||||
if attempt < maxRetries {
|
||||
continue
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
if attempt < maxRetries {
|
||||
continue
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Retry только для transient-ошибок и только для GET
|
||||
if resp.StatusCode >= 400 {
|
||||
if attempt < maxRetries && method == "GET" && isRetryable(resp.StatusCode) {
|
||||
lastErr = formatAPIError(resp.StatusCode, respBody)
|
||||
continue
|
||||
}
|
||||
return nil, nil, formatAPIError(resp.StatusCode, respBody)
|
||||
}
|
||||
|
||||
return respBody, resp.Header, nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, c.ApiEndpoint+path, body)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, nil, fmt.Errorf("doRequest failed after %d retries: %w", maxRetries, lastErr)
|
||||
}
|
||||
|
||||
req.Close = true
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if c.ApiToken != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.ApiToken)
|
||||
}
|
||||
|
||||
resp, err := c.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, nil, formatAPIError(resp.StatusCode, respBody)
|
||||
}
|
||||
|
||||
return respBody, resp.Header, nil
|
||||
// isRetryable returns true for transient HTTP errors that can be retried.
|
||||
func isRetryable(statusCode int) bool {
|
||||
return statusCode == http.StatusTooManyRequests || // 429
|
||||
statusCode == http.StatusServiceUnavailable || // 503
|
||||
statusCode == http.StatusBadGateway || // 502
|
||||
statusCode == http.StatusGatewayTimeout // 504
|
||||
}
|
||||
|
||||
func (c *UniversalClient) postIgnoreResponse(ctx context.Context, path string, payload interface{}, returnLocation bool) (string, error) {
|
||||
|
||||
Reference in New Issue
Block a user