@@ -22,6 +22,8 @@ public class GithubClient
22
22
23
23
private const string DEFAULT_RATE_LIMIT_REMAINING = "5000" ;
24
24
private const int MILLISECONDS_PER_SECOND = 1000 ;
25
+ private const int SECONDARY_RATE_LIMIT_MAX_RETRIES = 3 ;
26
+ private const int SECONDARY_RATE_LIMIT_DEFAULT_DELAY = 60 ; // 60 seconds default delay
25
27
26
28
public GithubClient ( OctoLogger log , HttpClient httpClient , IVersionProvider versionProvider , RetryPolicy retryPolicy , DateTimeProvider dateTimeProvider , string personalAccessToken )
27
29
{
@@ -159,7 +161,8 @@ public virtual async Task<string> PatchAsync(string url, object body, Dictionary
159
161
string url ,
160
162
object body = null ,
161
163
HttpStatusCode expectedStatus = HttpStatusCode . OK ,
162
- Dictionary < string , string > customHeaders = null )
164
+ Dictionary < string , string > customHeaders = null ,
165
+ int retryCount = 0 )
163
166
{
164
167
await ApplyRetryDelayAsync ( ) ;
165
168
_log . LogVerbose ( $ "HTTP { httpMethod } : { url } ") ;
@@ -199,9 +202,15 @@ public virtual async Task<string> PatchAsync(string url, object body, Dictionary
199
202
SetRetryDelay ( headers ) ;
200
203
}
201
204
205
+ // Check for secondary rate limits before handling primary rate limits
206
+ if ( IsSecondaryRateLimit ( response . StatusCode , content ) )
207
+ {
208
+ return await HandleSecondaryRateLimit ( httpMethod , url , body , expectedStatus , customHeaders , response , content , headers , retryCount ) ;
209
+ }
210
+
202
211
if ( response . StatusCode == HttpStatusCode . Forbidden && _retryDelay > 0 )
203
212
{
204
- ( content , headers ) = await SendAsync ( httpMethod , url , body , expectedStatus , customHeaders ) ;
213
+ ( content , headers ) = await SendAsync ( httpMethod , url , body , expectedStatus , customHeaders , retryCount ) ;
205
214
}
206
215
else if ( expectedStatus == HttpStatusCode . OK )
207
216
{
@@ -280,4 +289,78 @@ private void EnsureSuccessGraphQLResponse(JObject response)
280
289
throw new OctoshiftCliException ( $ "{ errorMessage ?? "UNKNOWN" } ") ;
281
290
}
282
291
}
292
+
293
+ private bool IsSecondaryRateLimit ( HttpStatusCode statusCode , string content )
294
+ {
295
+ // Secondary rate limits return 403 or 429
296
+ if ( statusCode is not HttpStatusCode . Forbidden and not HttpStatusCode . TooManyRequests )
297
+ {
298
+ return false ;
299
+ }
300
+
301
+ // Check if this is a primary rate limit (which we handle separately)
302
+ if ( content . ToUpper ( ) . Contains ( "API RATE LIMIT EXCEEDED" ) )
303
+ {
304
+ return false ;
305
+ }
306
+
307
+ // Common secondary rate limit error patterns
308
+ var contentUpper = content . ToUpper ( ) ;
309
+ return contentUpper . Contains ( "SECONDARY RATE LIMIT" ) ||
310
+ contentUpper . Contains ( "ABUSE DETECTION" ) ||
311
+ contentUpper . Contains ( "YOU HAVE TRIGGERED AN ABUSE DETECTION MECHANISM" ) ||
312
+ statusCode == HttpStatusCode . TooManyRequests ;
313
+ }
314
+
315
+ private async Task < ( string Content , KeyValuePair < string , IEnumerable < string > > [ ] ResponseHeaders ) > HandleSecondaryRateLimit (
316
+ HttpMethod httpMethod ,
317
+ string url ,
318
+ object body ,
319
+ HttpStatusCode expectedStatus ,
320
+ Dictionary < string , string > customHeaders ,
321
+ HttpResponseMessage response ,
322
+ string content ,
323
+ KeyValuePair < string , IEnumerable < string > > [ ] headers ,
324
+ int retryCount = 0 )
325
+ {
326
+ if ( retryCount >= SECONDARY_RATE_LIMIT_MAX_RETRIES )
327
+ {
328
+ throw new OctoshiftCliException ( $ "Secondary rate limit exceeded. Maximum retries ({ SECONDARY_RATE_LIMIT_MAX_RETRIES } ) reached. Please wait before retrying your request.") ;
329
+ }
330
+
331
+ var delaySeconds = GetSecondaryRateLimitDelay ( headers , retryCount ) ;
332
+
333
+ _log . LogWarning ( $ "Secondary rate limit detected (attempt { retryCount + 1 } /{ SECONDARY_RATE_LIMIT_MAX_RETRIES } ). Waiting { delaySeconds } seconds before retrying...") ;
334
+
335
+ await Task . Delay ( delaySeconds * MILLISECONDS_PER_SECOND ) ;
336
+
337
+ return await SendAsync ( httpMethod , url , body , expectedStatus , customHeaders , retryCount + 1 ) ;
338
+ }
339
+
340
+ private int GetSecondaryRateLimitDelay ( KeyValuePair < string , IEnumerable < string > > [ ] headers , int retryCount )
341
+ {
342
+ // First check for retry-after header
343
+ var retryAfterHeader = ExtractHeaderValue ( "Retry-After" , headers ) ;
344
+ if ( ! string . IsNullOrEmpty ( retryAfterHeader ) && int . TryParse ( retryAfterHeader , out var retryAfterSeconds ) )
345
+ {
346
+ return retryAfterSeconds ;
347
+ }
348
+
349
+ // Then check if x-ratelimit-remaining is 0 and use x-ratelimit-reset
350
+ var rateLimitRemaining = GetRateLimitRemaining ( headers ) ;
351
+ if ( rateLimitRemaining <= 0 )
352
+ {
353
+ var resetUnixSeconds = GetRateLimitReset ( headers ) ;
354
+ var currentUnixSeconds = _dateTimeProvider . CurrentUnixTimeSeconds ( ) ;
355
+ var delayFromReset = ( int ) ( resetUnixSeconds - currentUnixSeconds ) ;
356
+
357
+ if ( delayFromReset > 0 )
358
+ {
359
+ return delayFromReset ;
360
+ }
361
+ }
362
+
363
+ // Otherwise use exponential backoff: 1m → 2m → 4m
364
+ return SECONDARY_RATE_LIMIT_DEFAULT_DELAY * ( int ) Math . Pow ( 2 , retryCount ) ;
365
+ }
283
366
}
0 commit comments