@@ -7,30 +7,53 @@ import (
77 "net/http"
88 urlpkg "net/url"
99 "os"
10+ "strconv"
1011 "strings"
12+ "time"
1113
1214 "github.com/Cloverhound/webex-cli/internal/config"
1315)
1416
1517// ErrDryRun is returned when a write operation is intercepted by --dry-run mode.
1618var ErrDryRun = errors .New ("dry run: no changes made" )
1719
18- // Do executes an HTTP request. On a 401, it attempts to refresh the token and retry once.
20+ // Do executes an HTTP request.
21+ // On a 401 it refreshes the token and retries once.
22+ // On a 429 it reads Retry-After and retries up to 3 times.
1923func Do (req * Request ) ([]byte , int , error ) {
20- body , status , err := doOnce (req )
24+ body , status , headers , err := doOnce (req )
25+
2126 if status == 401 && config .TokenRefresher != nil {
2227 newToken , refreshErr := config .TokenRefresher ()
2328 if refreshErr != nil {
2429 return body , status , err
2530 }
2631 config .SetToken (newToken )
27- return doOnce (req )
32+ body , status , headers , err = doOnce (req )
33+ }
34+
35+ for retries := 0 ; status == 429 && retries < 3 ; retries ++ {
36+ wait := retryAfterDuration (headers .Get ("Retry-After" ))
37+ fmt .Fprintf (os .Stderr , "Rate limited (429). Retrying in %s...\n " , wait .Round (time .Second ))
38+ time .Sleep (wait )
39+ body , status , headers , err = doOnce (req )
2840 }
41+
2942 return body , status , err
3043}
3144
45+ // retryAfterDuration parses the Retry-After header value (seconds integer) and
46+ // returns the duration to wait. Defaults to 5 seconds if the header is absent
47+ // or unparseable.
48+ func retryAfterDuration (header string ) time.Duration {
49+ if secs , err := strconv .Atoi (strings .TrimSpace (header )); err == nil && secs > 0 {
50+ return time .Duration (secs ) * time .Second
51+ }
52+ return 5 * time .Second
53+ }
54+
3255// doOnce executes a single HTTP request without retry.
33- func doOnce (req * Request ) ([]byte , int , error ) {
56+ func doOnce (req * Request ) ([]byte , int , http. Header , error ) {
3457 // Build URL
3558 url := req .baseURL + req .path
3659 for k , v := range req .pathParams {
@@ -58,7 +81,7 @@ func doOnce(req *Request) ([]byte, int, error) {
5881
5982 httpReq , err := http .NewRequest (req .method , url , bodyReader )
6083 if err != nil {
61- return nil , 0 , fmt .Errorf ("building request: %w" , err )
84+ return nil , 0 , nil , fmt .Errorf ("building request: %w" , err )
6285 }
6386
6487 // Auth
@@ -99,29 +122,29 @@ func doOnce(req *Request) ([]byte, int, error) {
99122 if req .bodyRaw != "" {
100123 fmt .Fprintf (os .Stderr , "[DRY RUN] Body: %s\n " , req .bodyRaw )
101124 }
102- return nil , 0 , ErrDryRun
125+ return nil , 0 , nil , ErrDryRun
103126 }
104127
105128 resp , err := http .DefaultClient .Do (httpReq )
106129 if err != nil {
107- return nil , 0 , fmt .Errorf ("executing request: %w" , err )
130+ return nil , 0 , nil , fmt .Errorf ("executing request: %w" , err )
108131 }
109132 defer resp .Body .Close ()
110133
111134 body , err := io .ReadAll (resp .Body )
112135 if err != nil {
113- return nil , resp .StatusCode , fmt .Errorf ("reading response: %w" , err )
136+ return nil , resp .StatusCode , resp . Header , fmt .Errorf ("reading response: %w" , err )
114137 }
115138
116139 if config .Debug () {
117140 fmt .Fprintf (os .Stderr , "DEBUG: Response %d (%d bytes)\n " , resp .StatusCode , len (body ))
118141 }
119142
120143 if resp .StatusCode >= 400 {
121- return body , resp .StatusCode , fmt .Errorf ("API error %d: %s" , resp .StatusCode , truncate (string (body ), 500 ))
144+ return body , resp .StatusCode , resp . Header , fmt .Errorf ("API error %d: %s" , resp .StatusCode , truncate (string (body ), 500 ))
122145 }
123146
124- return body , resp .StatusCode , nil
147+ return body , resp .StatusCode , resp . Header , nil
125148}
126149
127150func truncate (s string , n int ) string {
0 commit comments