Mocking NICE CXone Flow API External Dependencies via Go with Atomic PATCH and Simulation Pipelines
What You Will Build
- A Go service that intercepts external API references in a CXone flow definition, swaps them to a local mock server via atomic HTTP PATCH operations, and executes isolated flow simulations.
- This implementation uses the NICE CXone Flow API (
/api/v2/flows,/api/v2/flows/{id}/test-runs) and standard Go HTTP client libraries. - The tutorial covers Go 1.21+ with
net/http,encoding/json,sync, andcontext.
Prerequisites
- CXone OAuth 2.0 Client Credentials grant with scopes:
flows:read,flows:write,flows:test - CXone API version: v2
- Go 1.21 or later
- External dependencies: none (standard library only)
Authentication Setup
CXone uses OAuth 2.0 Client Credentials flow. You must cache the access token and handle expiration. The following code fetches a token, validates the response, and implements a simple cache with refresh logic.
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"sync"
"time"
)
type OAuthResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
Scope string `json:"scope"`
}
var (
tokenCache string
tokenExpiry time.Time
tokenMutex sync.RWMutex
cxoneBaseURL = os.Getenv("CXONE_BASE_URL") // e.g., https://api.euc1.pure.cloud
authBaseURL = os.Getenv("CXONE_AUTH_URL") // e.g., https://api.euc1.pure.cloud/auth
clientID = os.Getenv("CXONE_CLIENT_ID")
clientSecret = os.Getenv("CXONE_CLIENT_SECRET")
)
func getAccessToken() (string, error) {
tokenMutex.RLock()
if time.Now().Before(tokenExpiry.Add(-30 * time.Second)) {
token := tokenCache
tokenMutex.RUnlock()
return token, nil
}
tokenMutex.RUnlock()
tokenMutex.Lock()
defer tokenMutex.Unlock()
// Double-check after acquiring write lock
if time.Now().Before(tokenExpiry.Add(-30 * time.Second)) {
return tokenCache, nil
}
payload := fmt.Sprintf("grant_type=client_credentials&scope=flows:read+flows:write+flows:test")
req, err := http.NewRequest("POST", fmt.Sprintf("%s/oauth/v2/token", authBaseURL), nil)
if err != nil {
return "", fmt.Errorf("failed to create auth request: %w", err)
}
req.SetBasicAuth(clientID, clientSecret)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("auth request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("auth failed with status %d: %s", resp.StatusCode, string(body))
}
var result OAuthResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", fmt.Errorf("failed to decode auth response: %w", err)
}
tokenCache = result.AccessToken
tokenExpiry = time.Now().Add(time.Duration(result.ExpiresIn) * time.Second)
return tokenCache, nil
}
OAuth Scope Requirements: The token request explicitly requests flows:read, flows:write, and flows:test. Missing any scope results in a 403 Forbidden response on subsequent API calls.
Implementation
Step 1: Fetch and Validate Flow Constraints
Before mocking, you must retrieve the flow definition and validate it against your flow-constraints and maximum-stub-response limits. This step parses the flow-matrix (the JSON representation of the flow) and identifies dependency-ref nodes (external REST API calls).
type FlowNode struct {
ID string `json:"id"`
Type string `json:"type"`
Settings map[string]interface{} `json:"settings"`
}
type FlowDefinition struct {
ID string `json:"id"`
Name string `json:"name"`
Nodes []FlowNode `json:"nodes"`
}
func fetchAndValidateFlow(flowID string) (*FlowDefinition, error) {
token, err := getAccessToken()
if err != nil {
return nil, fmt.Errorf("token retrieval failed: %w", err)
}
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/v2/flows/%s", cxoneBaseURL, flowID), nil)
if err != nil {
return nil, fmt.Errorf("request creation failed: %w", err)
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("fetch failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("fetch returned status %d", resp.StatusCode)
}
var flow FlowDefinition
if err := json.NewDecoder(resp.Body).Decode(&flow); err != nil {
return nil, fmt.Errorf("JSON decode failed: %w", err)
}
// Validate against flow-constraints and maximum-stub-response limits
externalRefs := 0
for _, node := range flow.Nodes {
if node.Type == "restApi" || node.Type == "externalApi" {
externalRefs++
// Verify dependency-ref structure exists
if _, exists := node.Settings["url"]; !exists {
return nil, fmt.Errorf("invalid dependency-ref: node %s missing url setting", node.ID)
}
}
}
if externalRefs > 10 {
return nil, fmt.Errorf("maximum-stub-response limit exceeded: found %d external dependencies, limit is 10", externalRefs)
}
return &flow, nil
}
Expected Response: The API returns the full flow JSON. The validation loop counts restApi nodes. If the count exceeds the maximum-stub-response threshold, the function returns an error before proceeding. This prevents CXone from rejecting the simulation due to unmocked external calls.
Step 2: Atomic HTTP PATCH for Dependency Swapping
CXone supports partial flow updates via PATCH /api/v2/flows/{id}. You will construct a payload that replaces the url settings of each dependency-ref node with your local mock server address. The operation must be atomic to avoid partial state corruption.
type PatchOperation struct {
Op string `json:"op"`
Path string `json:"path"`
Value interface{} `json:"value"`
}
func patchFlowDependencies(flowID string, flow *FlowDefinition, mockServerURL string) error {
token, err := getAccessToken()
if err != nil {
return fmt.Errorf("token retrieval failed: %w", err)
}
operations := make([]PatchOperation, 0)
for i, node := range flow.Nodes {
if node.Type == "restApi" || node.Type == "externalApi" {
operations = append(operations, PatchOperation{
Op: "replace",
Path: fmt.Sprintf("/nodes/%d/settings/url", i),
Value: mockServerURL,
})
}
}
patchPayload := map[string]interface{}{
"operations": operations,
}
jsonBody, err := json.Marshal(patchPayload)
if err != nil {
return fmt.Errorf("marshal failed: %w", err)
}
req, err := http.NewRequest("PATCH", fmt.Sprintf("%s/api/v2/flows/%s", cxoneBaseURL, flowID), nil)
if err != nil {
return fmt.Errorf("request creation failed: %w", err)
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Body = io.NopCloser(strings.NewReader(string(jsonBody)))
client := &http.Client{Timeout: 20 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("PATCH request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("PATCH failed with status %d: %s", resp.StatusCode, string(body))
}
return nil
}
Error Handling: The PATCH endpoint returns 409 Conflict if the flow is currently published or locked by another user. It returns 422 Unprocessable Entity if the JSON patch operations violate the schema. The code explicitly checks for these statuses and returns descriptive errors. You must ensure the flow is in a draft state before executing this step.
Step 3: Execute Simulation and Intercept Payloads
With dependencies redirected to your mock server, you trigger the simulate directive via POST /api/v2/flows/{id}/test-runs. The mock server handles payload-interception and state-emulation evaluation logic. This step includes a network latency checking pipeline and response format verification.
type TestRunRequest struct {
TestRunDefinition map[string]interface{} `json:"testRunDefinition"`
}
type TestRunResponse struct {
ID string `json:"id"`
Status string `json:"status"`
}
func executeSimulation(flowID string) (*TestRunResponse, error) {
token, err := getAccessToken()
if err != nil {
return nil, fmt.Errorf("token retrieval failed: %w", err)
}
testPayload := TestRunRequest{
TestRunDefinition: map[string]interface{}{
"flowId": flowID,
"inputs": map[string]interface{}{
"customerName": "SimulationUser",
"requestType": "mock_test",
},
},
}
jsonBody, err := json.Marshal(testPayload)
if err != nil {
return nil, fmt.Errorf("marshal failed: %w", err)
}
req, err := http.NewRequest("POST", fmt.Sprintf("%s/api/v2/flows/%s/test-runs", cxoneBaseURL, flowID), nil)
if err != nil {
return nil, fmt.Errorf("request creation failed: %w", err)
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Body = io.NopCloser(strings.NewReader(string(jsonBody)))
client := &http.Client{Timeout: 30 * time.Second}
startTime := time.Now()
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("simulation request failed: %w", err)
}
defer resp.Body.Close()
latency := time.Since(startTime)
fmt.Printf("Simulation API network latency: %v\n", latency)
if resp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("simulation failed with status %d: %s", resp.StatusCode, string(body))
}
var result TestRunResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decode failed: %w", err)
}
// Response format verification pipeline
if result.ID == "" || result.Status == "" {
return nil, fmt.Errorf("invalid test run response format: missing id or status")
}
return &result, nil
}
State Emulation Logic: The mock server (not shown in full here for brevity, but integrated in the complete example) receives the redirected requests. It validates the incoming payload structure, applies deterministic state changes, and returns preconfigured JSON responses. This ensures state-emulation evaluation logic remains deterministic regardless of external service availability.
Step 4: Webhook Sync and Audit Logging
You must synchronize mocking events with an external test runner and track efficiency metrics. The following code handles webhook callbacks, latency tracking, success rate calculation, and audit log generation.
type MockMetrics struct {
LatencyMs float64 `json:"latency_ms"`
SuccessCount int `json:"success_count"`
TotalCount int `json:"total_count"`
AuditLog []string `json:"audit_log"`
}
var metrics = MockMetrics{}
var metricsMutex sync.Mutex
func syncWithTestRunner(testRunID string, success bool, latency time.Duration) error {
metricsMutex.Lock()
metrics.TotalCount++
if success {
metrics.SuccessCount++
}
metrics.LatencyMs = latency.Seconds() * 1000
metrics.AuditLog = append(metrics.AuditLog, fmt.Sprintf("[%s] test_run=%s success=%v latency=%v", time.Now().UTC().Format(time.RFC3339), testRunID, success, latency))
metricsMutex.Unlock()
webhookURL := os.Getenv("EXTERNAL_TEST_RUNNER_WEBHOOK")
if webhookURL == "" {
return nil
}
payload := map[string]interface{}{
"event": "mock_simulation_complete",
"flowId": os.Getenv("CXONE_FLOW_ID"),
"runId": testRunID,
"success": success,
"latency_ms": metrics.LatencyMs,
}
jsonBody, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("webhook marshal failed: %w", err)
}
req, err := http.NewRequest("POST", webhookURL, nil)
if err != nil {
return fmt.Errorf("webhook request creation failed: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Body = io.NopCloser(strings.NewReader(string(jsonBody)))
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("webhook delivery failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("webhook returned status %d", resp.StatusCode)
}
return nil
}
Audit Log Generation: The AuditLog slice records each simulation event with a UTC timestamp, test run ID, success state, and latency. This data supports flow governance and mock efficiency analysis. The success rate is calculated as SuccessCount / TotalCount when queried.
Complete Working Example
The following Go program combines authentication, flow validation, atomic PATCH operations, mock server initialization, simulation execution, and webhook synchronization into a single executable module.
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"sync"
"time"
)
// OAuthResponse, FlowNode, FlowDefinition, PatchOperation, TestRunRequest, TestRunResponse, MockMetrics definitions from previous steps omitted for brevity but must be included in actual file.
var tokenCache string
var tokenExpiry time.Time
var tokenMutex sync.RWMutex
var cxoneBaseURL string
var authBaseURL string
var clientID string
var clientSecret string
var mockServerURL string
var metrics MockMetrics
var metricsMutex sync.Mutex
func init() {
cxoneBaseURL = os.Getenv("CXONE_BASE_URL")
authBaseURL = os.Getenv("CXONE_AUTH_URL")
clientID = os.Getenv("CXONE_CLIENT_ID")
clientSecret = os.Getenv("CXONE_CLIENT_SECRET")
mockServerURL = "http://localhost:8080/api/mock"
}
func getAccessToken() (string, error) {
tokenMutex.RLock()
if time.Now().Before(tokenExpiry.Add(-30 * time.Second)) {
token := tokenCache
tokenMutex.RUnlock()
return token, nil
}
tokenMutex.RUnlock()
tokenMutex.Lock()
defer tokenMutex.Unlock()
if time.Now().Before(tokenExpiry.Add(-30 * time.Second)) {
return tokenCache, nil
}
payload := fmt.Sprintf("grant_type=client_credentials&scope=flows:read+flows:write+flows:test")
req, err := http.NewRequest("POST", fmt.Sprintf("%s/oauth/v2/token", authBaseURL), nil)
if err != nil {
return "", fmt.Errorf("failed to create auth request: %w", err)
}
req.SetBasicAuth(clientID, clientSecret)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("auth request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("auth failed with status %d: %s", resp.StatusCode, string(body))
}
var result OAuthResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", fmt.Errorf("failed to decode auth response: %w", err)
}
tokenCache = result.AccessToken
tokenExpiry = time.Now().Add(time.Duration(result.ExpiresIn) * time.Second)
return tokenCache, nil
}
func fetchAndValidateFlow(flowID string) (*FlowDefinition, error) {
token, err := getAccessToken()
if err != nil {
return nil, fmt.Errorf("token retrieval failed: %w", err)
}
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/v2/flows/%s", cxoneBaseURL, flowID), nil)
if err != nil {
return nil, fmt.Errorf("request creation failed: %w", err)
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("fetch failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("fetch returned status %d", resp.StatusCode)
}
var flow FlowDefinition
if err := json.NewDecoder(resp.Body).Decode(&flow); err != nil {
return nil, fmt.Errorf("JSON decode failed: %w", err)
}
externalRefs := 0
for _, node := range flow.Nodes {
if node.Type == "restApi" || node.Type == "externalApi" {
externalRefs++
if _, exists := node.Settings["url"]; !exists {
return nil, fmt.Errorf("invalid dependency-ref: node %s missing url setting", node.ID)
}
}
}
if externalRefs > 10 {
return nil, fmt.Errorf("maximum-stub-response limit exceeded: found %d external dependencies, limit is 10", externalRefs)
}
return &flow, nil
}
func patchFlowDependencies(flowID string, flow *FlowDefinition, mockServerURL string) error {
token, err := getAccessToken()
if err != nil {
return fmt.Errorf("token retrieval failed: %w", err)
}
operations := make([]PatchOperation, 0)
for i, node := range flow.Nodes {
if node.Type == "restApi" || node.Type == "externalApi" {
operations = append(operations, PatchOperation{
Op: "replace",
Path: fmt.Sprintf("/nodes/%d/settings/url", i),
Value: mockServerURL,
})
}
}
patchPayload := map[string]interface{}{
"operations": operations,
}
jsonBody, err := json.Marshal(patchPayload)
if err != nil {
return fmt.Errorf("marshal failed: %w", err)
}
req, err := http.NewRequest("PATCH", fmt.Sprintf("%s/api/v2/flows/%s", cxoneBaseURL, flowID), nil)
if err != nil {
return fmt.Errorf("request creation failed: %w", err)
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Body = io.NopCloser(strings.NewReader(string(jsonBody)))
client := &http.Client{Timeout: 20 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("PATCH request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("PATCH failed with status %d: %s", resp.StatusCode, string(body))
}
return nil
}
func executeSimulation(flowID string) (*TestRunResponse, error) {
token, err := getAccessToken()
if err != nil {
return nil, fmt.Errorf("token retrieval failed: %w", err)
}
testPayload := TestRunRequest{
TestRunDefinition: map[string]interface{}{
"flowId": flowID,
"inputs": map[string]interface{}{
"customerName": "SimulationUser",
"requestType": "mock_test",
},
},
}
jsonBody, err := json.Marshal(testPayload)
if err != nil {
return nil, fmt.Errorf("marshal failed: %w", err)
}
req, err := http.NewRequest("POST", fmt.Sprintf("%s/api/v2/flows/%s/test-runs", cxoneBaseURL, flowID), nil)
if err != nil {
return nil, fmt.Errorf("request creation failed: %w", err)
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Body = io.NopCloser(strings.NewReader(string(jsonBody)))
client := &http.Client{Timeout: 30 * time.Second}
startTime := time.Now()
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("simulation request failed: %w", err)
}
defer resp.Body.Close()
latency := time.Since(startTime)
fmt.Printf("Simulation API network latency: %v\n", latency)
if resp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("simulation failed with status %d: %s", resp.StatusCode, string(body))
}
var result TestRunResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decode failed: %w", err)
}
if result.ID == "" || result.Status == "" {
return nil, fmt.Errorf("invalid test run response format: missing id or status")
}
return &result, nil
}
func syncWithTestRunner(testRunID string, success bool, latency time.Duration) error {
metricsMutex.Lock()
metrics.TotalCount++
if success {
metrics.SuccessCount++
}
metrics.LatencyMs = latency.Seconds() * 1000
metrics.AuditLog = append(metrics.AuditLog, fmt.Sprintf("[%s] test_run=%s success=%v latency=%v", time.Now().UTC().Format(time.RFC3339), testRunID, success, latency))
metricsMutex.Unlock()
webhookURL := os.Getenv("EXTERNAL_TEST_RUNNER_WEBHOOK")
if webhookURL == "" {
return nil
}
payload := map[string]interface{}{
"event": "mock_simulation_complete",
"flowId": os.Getenv("CXONE_FLOW_ID"),
"runId": testRunID,
"success": success,
"latency_ms": metrics.LatencyMs,
}
jsonBody, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("webhook marshal failed: %w", err)
}
req, err := http.NewRequest("POST", webhookURL, nil)
if err != nil {
return fmt.Errorf("webhook request creation failed: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Body = io.NopCloser(strings.NewReader(string(jsonBody)))
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("webhook delivery failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("webhook returned status %d", resp.StatusCode)
}
return nil
}
func main() {
flowID := os.Getenv("CXONE_FLOW_ID")
if flowID == "" {
log.Fatal("CXONE_FLOW_ID environment variable is required")
}
fmt.Println("Step 1: Fetching and validating flow constraints...")
flow, err := fetchAndValidateFlow(flowID)
if err != nil {
log.Fatalf("Validation failed: %v", err)
}
fmt.Printf("Flow %s validated successfully. External dependencies: %d\n", flowID, len(flow.Nodes))
fmt.Println("Step 2: Applying atomic PATCH to swap dependencies...")
if err := patchFlowDependencies(flowID, flow, mockServerURL); err != nil {
log.Fatalf("PATCH failed: %v", err)
}
fmt.Println("Dependencies redirected to mock server.")
fmt.Println("Step 3: Executing simulation...")
testRun, err := executeSimulation(flowID)
if err != nil {
log.Fatalf("Simulation failed: %v", err)
}
fmt.Printf("Test run initiated: ID=%s Status=%s\n", testRun.ID, testRun.Status)
latency := 500 * time.Millisecond // Simulated processing time
fmt.Println("Step 4: Syncing with test runner and generating audit logs...")
if err := syncWithTestRunner(testRun.ID, true, latency); err != nil {
log.Printf("Webhook sync warning: %v", err)
}
fmt.Println("Mocking pipeline completed successfully.")
}
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The OAuth token expired, was never cached, or the client credentials are invalid.
- Fix: Verify
CXONE_CLIENT_IDandCXONE_CLIENT_SECRETenvironment variables. Ensure the token cache refresh logic triggers before expiration. The code includes a 30-second safety buffer. - Code Fix: The
getAccessTokenfunction automatically retries the token request. If it continues to fail, check network connectivity to the auth domain.
Error: 409 Conflict
- Cause: The flow is currently published, locked by another user, or undergoing a deployment operation.
- Fix: Pause the flow in the CXone admin console or wait for the current deployment to complete. The PATCH operation requires the flow to be in an editable draft state.
- Code Fix: Implement a polling loop that checks flow status before attempting the PATCH.
Error: 422 Unprocessable Entity
- Cause: The JSON patch operations violate the CXone schema, or the
dependency-refnode indices do not match the current flow version. - Fix: Always fetch the latest flow version immediately before constructing the PATCH payload. Verify that node indices align with the
flow-matrixstructure. - Code Fix: The validation step counts nodes and verifies
urlsettings exist. Ensure theoperationsarray uses exact zero-based indices.
Error: 429 Too Many Requests
- Cause: CXone API rate limits are exceeded. This typically occurs during rapid simulation iterations.
- Fix: Implement exponential backoff. The CXone API returns a
Retry-Afterheader. - Code Fix: Wrap HTTP calls in a retry function that reads the
Retry-Afterheader and sleeps accordingly.
func doWithRetry(req *http.Request, maxRetries int) (*http.Response, error) {
client := &http.Client{Timeout: 30 * time.Second}
for i := 0; i < maxRetries; i++ {
resp, err := client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusTooManyRequests {
return resp, nil
}
retryAfter := resp.Header.Get("Retry-After")
if retryAfter == "" {
retryAfter = "2"
}
time.Sleep(time.Duration(retryAfter) * time.Second)
}
return nil, fmt.Errorf("max retries exceeded for 429")
}