88 "fmt"
99 "io"
1010 "net/http"
11+ "strconv"
1112 "strings"
1213 "time"
1314
@@ -21,6 +22,20 @@ type Client struct {
2122 httpClient * http.Client
2223}
2324
25+ // RateLimitError represents a 429 rate limit error with optional Retry-After information
26+ type RateLimitError struct {
27+ StatusCode int
28+ Message string
29+ RetryAfter string // Can be seconds or HTTP-date
30+ }
31+
32+ func (e * RateLimitError ) Error () string {
33+ if e .RetryAfter != "" {
34+ return fmt .Sprintf ("HTTP %d: %s (Retry-After: %s)" , e .StatusCode , e .Message , e .RetryAfter )
35+ }
36+ return fmt .Sprintf ("HTTP %d: %s" , e .StatusCode , e .Message )
37+ }
38+
2439// NewClient creates a new Pocket-ID API client
2540func NewClient (baseURL , apiToken string , skipTLSVerify bool , timeout int64 ) (* Client , error ) {
2641 if baseURL == "" {
@@ -58,9 +73,24 @@ func (c *Client) doRequestWithContext(ctx context.Context, method, endpoint stri
5873 var lastErr error
5974
6075 for attempt := 0 ; attempt <= maxRetries ; attempt ++ {
76+ var backoff time.Duration
77+
6178 if attempt > 0 {
6279 // Exponential backoff: 1s, 2s, 4s
63- backoff := time .Duration (1 << (attempt - 1 )) * time .Second
80+ backoff = time .Duration (1 << (attempt - 1 )) * time .Second
81+
82+ // Special handling for rate limit errors to respect Retry-After header
83+ if rateLimitErr , ok := lastErr .(* RateLimitError ); ok && rateLimitErr .RetryAfter != "" {
84+ retryAfterSeconds := parseRetryAfter (rateLimitErr .RetryAfter )
85+ if retryAfterSeconds > 0 {
86+ backoff = time .Duration (retryAfterSeconds ) * time .Second
87+ tflog .Info (ctx , "Rate limited, using Retry-After header" , map [string ]interface {}{
88+ "retry_after" : rateLimitErr .RetryAfter ,
89+ "backoff_seconds" : retryAfterSeconds ,
90+ })
91+ }
92+ }
93+
6494 tflog .Debug (ctx , "Retrying request after backoff" , map [string ]interface {}{
6595 "attempt" : attempt ,
6696 "backoff" : backoff .String (),
@@ -120,6 +150,16 @@ func isRetryableError(err error) bool {
120150 return true
121151 }
122152
153+ // 429 Too Many Requests is retryable (rate limiting)
154+ if strings .Contains (errStr , "HTTP 429" ) {
155+ return true
156+ }
157+
158+ // Check if it's a RateLimitError
159+ if _ , ok := err .(* RateLimitError ); ok {
160+ return true
161+ }
162+
123163 return false
124164}
125165
@@ -199,18 +239,56 @@ func (c *Client) doSingleRequest(ctx context.Context, method, endpoint string, b
199239 "status_code" : resp .StatusCode ,
200240 "raw_body" : string (respBody ),
201241 })
242+ // Handle rate limit errors with Retry-After header
243+ if resp .StatusCode == 429 {
244+ retryAfter := resp .Header .Get ("Retry-After" )
245+ return nil , & RateLimitError {
246+ StatusCode : resp .StatusCode ,
247+ Message : string (respBody ),
248+ RetryAfter : retryAfter ,
249+ }
250+ }
202251 return nil , fmt .Errorf ("HTTP %d: %s" , resp .StatusCode , string (respBody ))
203252 }
204253 tflog .Error (ctx , "API Error" , map [string ]interface {}{
205254 "status_code" : resp .StatusCode ,
206255 "error" : errResp .Error ,
207256 })
257+ // Handle rate limit errors with Retry-After header
258+ if resp .StatusCode == 429 {
259+ retryAfter := resp .Header .Get ("Retry-After" )
260+ return nil , & RateLimitError {
261+ StatusCode : resp .StatusCode ,
262+ Message : errResp .Error ,
263+ RetryAfter : retryAfter ,
264+ }
265+ }
208266 return nil , fmt .Errorf ("HTTP %d: %s" , resp .StatusCode , errResp .Error )
209267 }
210268
211269 return respBody , nil
212270}
213271
272+ // parseRetryAfter parses the Retry-After header value
273+ // It can be either a delay in seconds or an HTTP-date
274+ func parseRetryAfter (retryAfter string ) int {
275+ // First try to parse as integer seconds
276+ if seconds , err := strconv .Atoi (retryAfter ); err == nil && seconds > 0 {
277+ return seconds
278+ }
279+
280+ // Try to parse as HTTP-date
281+ if t , err := http .ParseTime (retryAfter ); err == nil {
282+ delay := time .Until (t ).Seconds ()
283+ if delay > 0 {
284+ return int (delay )
285+ }
286+ }
287+
288+ // Default to 60 seconds if we can't parse
289+ return 60
290+ }
291+
214292// OIDC Client methods
215293
216294// CreateClient creates a new OIDC client
0 commit comments