@@ -29,10 +29,12 @@ internal sealed class GithubClient : IDisposable
2929 private readonly Action ? _clearStatusMessage ;
3030 private readonly Dictionary < string , GithubRateLimitState > _rateLimitStates = new ( StringComparer . OrdinalIgnoreCase ) ;
3131 private readonly Dictionary < string , string > _fileContentCache = new ( StringComparer . OrdinalIgnoreCase ) ;
32+ private readonly Dictionary < string , GithubGistResponse > _gistCache = new ( StringComparer . OrdinalIgnoreCase ) ;
3233 private readonly Dictionary < string , string > _gistFileContentCache = new ( StringComparer . OrdinalIgnoreCase ) ;
3334 private readonly Dictionary < string , GithubRepositoryTreeResponse > _repositoryTreeCache = new ( StringComparer . OrdinalIgnoreCase ) ;
3435 private readonly object _rateLimitLock = new ( ) ;
3536 private readonly object _fileContentCacheLock = new ( ) ;
37+ private readonly object _gistCacheLock = new ( ) ;
3638 private readonly object _gistFileContentCacheLock = new ( ) ;
3739 private readonly object _repositoryTreeCacheLock = new ( ) ;
3840
@@ -534,14 +536,34 @@ private async Task<GithubGistSearchPage> GetGistSearchPageAsync(string query, in
534536
535537 private async Task < GithubGistResponse > GetGistAsync ( string gistId , CancellationToken cancellationToken )
536538 {
539+ lock ( _gistCacheLock )
540+ {
541+ if ( _gistCache . TryGetValue ( gistId , out var cachedGist ) )
542+ {
543+ return cachedGist ;
544+ }
545+ }
546+
537547 var requestUri = $ "gists/{ Uri . EscapeDataString ( gistId ) } ";
538- return await GetJsonAsync (
548+ var gist = await GetJsonAsync (
539549 requestUri ,
540550 "core" ,
541551 "GitHub gist fetch failed" ,
542552 GithubJsonSerializerContext . Default . GithubGistResponse ,
543553 cancellationToken ,
544554 allowAnonymousRetry : true ) ;
555+
556+ lock ( _gistCacheLock )
557+ {
558+ _gistCache . TryAdd ( gistId , gist ) ;
559+
560+ if ( ! string . IsNullOrWhiteSpace ( gist . Id ) )
561+ {
562+ _gistCache . TryAdd ( gist . Id , gist ) ;
563+ }
564+ }
565+
566+ return gist ;
545567 }
546568
547569 private async Task < IReadOnlyList < GithubSearchResult > > SearchGistAsync ( GithubGistResponse gist , IReadOnlyList < string > searchTerms , CancellationToken cancellationToken )
@@ -852,14 +874,15 @@ private async Task<HttpResponseMessage> SendGetAsync(
852874 bool allowAnonymousRetry ,
853875 bool useAuthentication = true )
854876 {
855- await WaitForKnownRateLimitAsync ( rateLimitResource , cancellationToken ) ;
856-
857877 if ( useAuthentication )
858878 {
859879 await EnsureFreshAccessTokenAsync ( forceRefresh : false , cancellationToken ) ;
860880 }
861881
862882 var authorization = _httpClient . DefaultRequestHeaders . Authorization ;
883+ var useAuthenticatedRateLimit = useAuthentication && authorization is not null ;
884+
885+ await WaitForKnownRateLimitAsync ( rateLimitResource , useAuthenticatedRateLimit , cancellationToken ) ;
863886
864887 if ( ! useAuthentication )
865888 {
@@ -880,7 +903,7 @@ private async Task<HttpResponseMessage> SendGetAsync(
880903 }
881904 }
882905
883- UpdateRateLimitState ( response , rateLimitResource ) ;
906+ UpdateRateLimitState ( response , rateLimitResource , useAuthenticatedRateLimit ) ;
884907
885908 if ( ! allowAnonymousRetry || response . IsSuccessStatusCode || authorization is null || ! useAuthentication )
886909 {
@@ -897,8 +920,9 @@ private async Task<HttpResponseMessage> SendGetAsync(
897920 try
898921 {
899922 _httpClient . DefaultRequestHeaders . Authorization = null ;
923+ await WaitForKnownRateLimitAsync ( rateLimitResource , useAuthentication : false , cancellationToken ) ;
900924 var anonymousResponse = await _httpClient . GetAsync ( requestUri , cancellationToken ) ;
901- UpdateRateLimitState ( anonymousResponse , rateLimitResource ) ;
925+ UpdateRateLimitState ( anonymousResponse , rateLimitResource , useAuthentication : false ) ;
902926 return anonymousResponse ;
903927 }
904928 finally
@@ -934,9 +958,11 @@ private async Task<bool> TryRefreshAccessTokenAsync(CancellationToken cancellati
934958 return _httpClient . DefaultRequestHeaders . Authorization is not null ;
935959 }
936960
937- private async Task WaitForKnownRateLimitAsync ( string rateLimitResource , CancellationToken cancellationToken )
961+ private async Task WaitForKnownRateLimitAsync ( string rateLimitResource , bool useAuthentication , CancellationToken cancellationToken )
938962 {
939- while ( TryGetKnownRateLimitDelay ( rateLimitResource , out var retryDelay ) )
963+ var rateLimitStateKey = CreateRateLimitStateKey ( rateLimitResource , useAuthentication ) ;
964+
965+ while ( TryGetKnownRateLimitDelay ( rateLimitStateKey , out var retryDelay ) )
940966 {
941967 if ( retryDelay > MaxAutomaticRateLimitDelay )
942968 {
@@ -974,7 +1000,7 @@ private bool TryGetKnownRateLimitDelay(string rateLimitResource, out TimeSpan re
9741000 return false ;
9751001 }
9761002
977- private void UpdateRateLimitState ( HttpResponseMessage response , string fallbackResource )
1003+ private void UpdateRateLimitState ( HttpResponseMessage response , string fallbackResource , bool useAuthentication )
9781004 {
9791005 var resource = TryGetHeaderValue ( response . Headers , "X-RateLimit-Resource" , out var resourceValue )
9801006 ? resourceValue !
@@ -995,12 +1021,19 @@ private void UpdateRateLimitState(HttpResponseMessage response, string fallbackR
9951021 return ;
9961022 }
9971023
1024+ var rateLimitStateKey = CreateRateLimitStateKey ( resource , useAuthentication ) ;
1025+
9981026 lock ( _rateLimitLock )
9991027 {
1000- _rateLimitStates [ resource ] = new GithubRateLimitState ( remaining , resetAt ) ;
1028+ _rateLimitStates [ rateLimitStateKey ] = new GithubRateLimitState ( remaining , resetAt ) ;
10011029 }
10021030 }
10031031
1032+ private static string CreateRateLimitStateKey ( string resource , bool useAuthentication )
1033+ {
1034+ return $ "{ ( useAuthentication ? "auth" : "anonymous" ) } :{ resource } ";
1035+ }
1036+
10041037 private bool TryGetRateLimitDelay (
10051038 HttpResponseMessage response ,
10061039 GithubErrorResponse ? errorResponse ,
@@ -1115,9 +1148,9 @@ private HttpRequestException CreateRateLimitException(HttpStatusCode statusCode,
11151148 builder . Append ( $ ". Retry after { FormatDelay ( retryDelay ) } ") ;
11161149 }
11171150
1118- if ( ! _hasAuthentication )
1151+ if ( ShouldSuggestPatForRateLimit ( rateLimitMessage ) )
11191152 {
1120- builder . Append ( ". Set GITHUB_ACCESS_TOKEN or pass --token to increase rate limits" ) ;
1153+ builder . Append ( ". For gist scans, use a GitHub PAT with --token or GITHUB_ACCESS_TOKEN to increase this limit; GitHub App installation tokens do not raise limits for arbitrary public gist reads " ) ;
11211154 }
11221155
11231156 if ( ! string . IsNullOrWhiteSpace ( rateLimitMessage ) )
@@ -1128,6 +1161,17 @@ private HttpRequestException CreateRateLimitException(HttpStatusCode statusCode,
11281161 return new HttpRequestException ( builder . ToString ( ) , null , statusCode ) ;
11291162 }
11301163
1164+ private bool ShouldSuggestPatForRateLimit ( string ? rateLimitMessage )
1165+ {
1166+ if ( ! _hasAuthentication )
1167+ {
1168+ return true ;
1169+ }
1170+
1171+ return ! string . IsNullOrWhiteSpace ( rateLimitMessage )
1172+ && rateLimitMessage . Contains ( "Authenticated requests get a higher rate limit" , StringComparison . OrdinalIgnoreCase ) ;
1173+ }
1174+
11311175 private string FormatGithubFailureMessage ( string failurePrefix , HttpStatusCode statusCode , string ? errorMessage )
11321176 {
11331177 var message = string . IsNullOrWhiteSpace ( errorMessage )
0 commit comments