@@ -3,15 +3,15 @@ use crate::{
33 config:: Config ,
44 updater:: { FetchRepositoriesResult , Repository , RepositoryForge , RepositoryName } ,
55} ;
6- use anyhow:: { Result , bail} ;
6+ use anyhow:: { Result , anyhow , bail} ;
77use async_trait:: async_trait;
88use chrono:: { DateTime , Utc } ;
99use docs_rs_utils:: APP_USER_AGENT ;
1010use reqwest:: {
11- Client as HttpClient ,
11+ Client as HttpClient , StatusCode ,
1212 header:: { ACCEPT , AUTHORIZATION , HeaderMap , HeaderValue , USER_AGENT } ,
1313} ;
14- use serde:: Deserialize ;
14+ use serde:: { Deserialize , Serialize } ;
1515use tracing:: { trace, warn} ;
1616
1717const GRAPHQL_UPDATE : & str = "query($ids: [ID!]!) {
@@ -155,7 +155,7 @@ impl RepositoryForge for GitHub {
155155 ( "RATE_LIMITED" , [ ] ) => {
156156 return Err ( RateLimitReached . into ( ) ) ;
157157 }
158- _ => anyhow :: bail!( "error updating repositories: {}" , error. message) ,
158+ _ => bail ! ( "error updating repositories: {}" , error. message) ,
159159 }
160160 }
161161
@@ -198,12 +198,24 @@ impl GitHub {
198198 . await ?;
199199
200200 let status = response. status ( ) ;
201-
202- if status. is_client_error ( ) || status. is_server_error ( ) {
203- let body = response. text ( ) . await ?;
204- bail ! ( "GitHub GraphQL response status: {}\n {}" , status, body) ;
201+ let body = response. text ( ) . await ?;
202+
203+ if status == StatusCode :: FORBIDDEN
204+ && let Ok ( api_error) = serde_json:: from_str :: < ApiError > ( & body)
205+ && ( api_error
206+ . documentation_url
207+ . contains ( "secondary-rate-limits" )
208+ || api_error. message . contains ( "secondary rate limit" ) )
209+ {
210+ Err ( RateLimitReached . into ( ) )
211+ } else if status. is_client_error ( ) || status. is_server_error ( ) {
212+ Err ( anyhow ! (
213+ "GitHub GraphQL response status: {}\n {}" ,
214+ status,
215+ body
216+ ) )
205217 } else {
206- Ok ( response . json ( ) . await ?)
218+ Ok ( serde_json :: from_str ( & body ) ?)
207219 }
208220 }
209221}
@@ -266,10 +278,17 @@ struct GraphIssues {
266278 total_count : i64 ,
267279}
268280
281+ #[ derive( Debug , Serialize , Deserialize ) ]
282+ struct ApiError {
283+ documentation_url : String ,
284+ message : String ,
285+ }
286+
269287#[ cfg( test) ]
270288mod tests {
271289 use crate :: {
272290 Config , GitHub , RateLimitReached ,
291+ github:: ApiError ,
273292 updater:: { RepositoryForge , repository_name} ,
274293 } ;
275294 use anyhow:: Result ;
@@ -417,4 +436,41 @@ mod tests {
417436
418437 Ok ( ( ) )
419438 }
439+
440+ #[ tokio:: test]
441+ async fn test_secondary_rate_limit ( ) -> Result < ( ) > {
442+ let config = github_config ( ) ?;
443+ let ( mut server, updater) = mock_server_and_github ( & config) . await ;
444+
445+ let _m1 = server
446+ . mock ( "POST" , "/graphql" )
447+ . with_header ( "content-type" , "application/json" )
448+ . with_status ( 403 )
449+ . with_body ( & serde_json:: to_string ( & ApiError {
450+ documentation_url : "https://docs.github.com/graphql/overview/\
451+ rate-limits-and-node-limits-for-the-graphql-api#secondary-rate-limits"
452+ . into ( ) ,
453+ message : "You have exceeded a secondary rate limit.
454+ Please wait a few minutes before you try again.
455+ For more on scraping GitHub and how it may affect your rights,
456+ please review our Terms of Service
457+ (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service)
458+ If you reach out to GitHub Support for help, please include the request ID
459+ ECEE:193CF9:5A5D684:1866A8EB:698779A9."
460+ . into ( ) ,
461+ } ) ?)
462+ . create ( ) ;
463+
464+ assert ! (
465+ updater
466+ . fetch_repository(
467+ & repository_name( "https://gitlab.com/foo/bar" ) . expect( "repository_name failed" ) ,
468+ )
469+ . await
470+ . unwrap_err( )
471+ . is:: <RateLimitReached >( )
472+ ) ;
473+
474+ Ok ( ( ) )
475+ }
420476}
0 commit comments