2222#include "object-file.h"
2323#include "odb.h"
2424#include "tempfile.h"
25+ #include "date.h"
26+ #include "trace2.h"
2527
2628static struct trace_key trace_curl = TRACE_KEY_INIT (CURL );
2729static int trace_curl_data = 1 ;
@@ -149,6 +151,11 @@ static char *cached_accept_language;
149151static char * http_ssl_backend ;
150152
151153static int http_schannel_check_revoke = 1 ;
154+
155+ static long http_retry_after = 0 ;
156+ static long http_max_retries = 0 ;
157+ static long http_max_retry_time = 300 ;
158+
152159/*
153160 * With the backend being set to `schannel`, setting sslCAinfo would override
154161 * the Certificate Store in cURL v7.60.0 and later, which is not what we want
@@ -209,7 +216,7 @@ static inline int is_hdr_continuation(const char *ptr, const size_t size)
209216 return size && (* ptr == ' ' || * ptr == '\t' );
210217}
211218
212- static size_t fwrite_wwwauth (char * ptr , size_t eltsize , size_t nmemb , void * p UNUSED )
219+ static size_t fwrite_wwwauth (char * ptr , size_t eltsize , size_t nmemb , void * p MAYBE_UNUSED )
213220{
214221 size_t size = eltsize * nmemb ;
215222 struct strvec * values = & http_auth .wwwauth_headers ;
@@ -575,6 +582,21 @@ static int http_options(const char *var, const char *value,
575582 return 0 ;
576583 }
577584
585+ if (!strcmp ("http.retryafter" , var )) {
586+ http_retry_after = git_config_int (var , value , ctx -> kvi );
587+ return 0 ;
588+ }
589+
590+ if (!strcmp ("http.maxretries" , var )) {
591+ http_max_retries = git_config_int (var , value , ctx -> kvi );
592+ return 0 ;
593+ }
594+
595+ if (!strcmp ("http.maxretrytime" , var )) {
596+ http_max_retry_time = git_config_int (var , value , ctx -> kvi );
597+ return 0 ;
598+ }
599+
578600 /* Fall back on the default ones */
579601 return git_default_config (var , value , ctx , data );
580602}
@@ -1422,6 +1444,10 @@ void http_init(struct remote *remote, const char *url, int proactive_auth)
14221444 set_long_from_env (& curl_tcp_keepintvl , "GIT_TCP_KEEPINTVL" );
14231445 set_long_from_env (& curl_tcp_keepcnt , "GIT_TCP_KEEPCNT" );
14241446
1447+ set_long_from_env (& http_retry_after , "GIT_HTTP_RETRY_AFTER" );
1448+ set_long_from_env (& http_max_retries , "GIT_HTTP_MAX_RETRIES" );
1449+ set_long_from_env (& http_max_retry_time , "GIT_HTTP_MAX_RETRY_TIME" );
1450+
14251451 curl_default = get_curl_handle ();
14261452}
14271453
@@ -1871,6 +1897,10 @@ static int handle_curl_result(struct slot_results *results)
18711897 }
18721898 return HTTP_REAUTH ;
18731899 }
1900+ } else if (results -> http_code == 429 ) {
1901+ trace2_data_intmax ("http" , the_repository , "http/429-retry-after" ,
1902+ results -> retry_after );
1903+ return HTTP_RATE_LIMITED ;
18741904 } else {
18751905 if (results -> http_connectcode == 407 )
18761906 credential_reject (the_repository , & proxy_auth );
@@ -1886,6 +1916,7 @@ int run_one_slot(struct active_request_slot *slot,
18861916 struct slot_results * results )
18871917{
18881918 slot -> results = results ;
1919+
18891920 if (!start_active_slot (slot )) {
18901921 xsnprintf (curl_errorstr , sizeof (curl_errorstr ),
18911922 "failed to start HTTP request" );
@@ -2119,15 +2150,19 @@ static void http_opt_request_remainder(CURL *curl, off_t pos)
21192150
21202151static int http_request (const char * url ,
21212152 void * result , int target ,
2122- const struct http_get_options * options )
2153+ struct http_get_options * options )
21232154{
2155+ static struct http_get_options empty_opts ;
21242156 struct active_request_slot * slot ;
2125- struct slot_results results ;
2157+ struct slot_results results = { . retry_after = -1 } ;
21262158 struct curl_slist * headers = http_copy_default_headers ();
21272159 struct strbuf buf = STRBUF_INIT ;
21282160 const char * accept_language ;
21292161 int ret ;
21302162
2163+ if (!options )
2164+ options = & empty_opts ;
2165+
21312166 slot = get_active_slot ();
21322167 curl_easy_setopt (slot -> curl , CURLOPT_HTTPGET , 1L );
21332168
@@ -2156,22 +2191,19 @@ static int http_request(const char *url,
21562191 headers = curl_slist_append (headers , accept_language );
21572192
21582193 strbuf_addstr (& buf , "Pragma:" );
2159- if (options && options -> no_cache )
2194+ if (options -> no_cache )
21602195 strbuf_addstr (& buf , " no-cache" );
2161- if (options && options -> initial_request &&
2196+ if (options -> initial_request &&
21622197 http_follow_config == HTTP_FOLLOW_INITIAL )
21632198 curl_easy_setopt (slot -> curl , CURLOPT_FOLLOWLOCATION , 1L );
21642199
21652200 headers = curl_slist_append (headers , buf .buf );
21662201
21672202 /* Add additional headers here */
2168- if (options && options -> extra_headers ) {
2203+ if (options -> extra_headers ) {
21692204 const struct string_list_item * item ;
2170- if (options && options -> extra_headers ) {
2171- for_each_string_list_item (item , options -> extra_headers ) {
2172- headers = curl_slist_append (headers , item -> string );
2173- }
2174- }
2205+ for_each_string_list_item (item , options -> extra_headers )
2206+ headers = curl_slist_append (headers , item -> string );
21752207 }
21762208
21772209 headers = http_append_auth_header (& http_auth , headers );
@@ -2183,15 +2215,26 @@ static int http_request(const char *url,
21832215
21842216 ret = run_one_slot (slot , & results );
21852217
2186- if (options && options -> content_type ) {
2218+ #ifdef GIT_CURL_HAVE_CURLINFO_RETRY_AFTER
2219+ if (ret == HTTP_RATE_LIMITED ) {
2220+ curl_off_t retry_after ;
2221+ if (curl_easy_getinfo (slot -> curl , CURLINFO_RETRY_AFTER ,
2222+ & retry_after ) == CURLE_OK && retry_after > 0 )
2223+ results .retry_after = (long )retry_after ;
2224+ }
2225+ #endif
2226+
2227+ options -> retry_after = results .retry_after ;
2228+
2229+ if (options -> content_type ) {
21872230 struct strbuf raw = STRBUF_INIT ;
21882231 curlinfo_strbuf (slot -> curl , CURLINFO_CONTENT_TYPE , & raw );
21892232 extract_content_type (& raw , options -> content_type ,
21902233 options -> charset );
21912234 strbuf_release (& raw );
21922235 }
21932236
2194- if (options && options -> effective_url )
2237+ if (options -> effective_url )
21952238 curlinfo_strbuf (slot -> curl , CURLINFO_EFFECTIVE_URL ,
21962239 options -> effective_url );
21972240
@@ -2253,30 +2296,72 @@ static int update_url_from_redirect(struct strbuf *base,
22532296 return 1 ;
22542297}
22552298
2256- static int http_request_reauth (const char * url ,
2299+ /*
2300+ * Compute the retry delay for an HTTP 429 response.
2301+ * Returns a negative value if configuration is invalid (delay exceeds
2302+ * http.maxRetryTime), otherwise returns the delay in seconds (>= 0).
2303+ */
2304+ static long handle_rate_limit_retry (long slot_retry_after )
2305+ {
2306+ /* Use the slot-specific retry_after value or configured default */
2307+ if (slot_retry_after >= 0 ) {
2308+ /* Check if retry delay exceeds maximum allowed */
2309+ if (slot_retry_after > http_max_retry_time ) {
2310+ error (_ ("response requested a delay greater than http.maxRetryTime (%ld > %ld seconds)" ),
2311+ slot_retry_after , http_max_retry_time );
2312+ trace2_data_string ("http" , the_repository ,
2313+ "http/429-error" , "exceeds-max-retry-time" );
2314+ trace2_data_intmax ("http" , the_repository ,
2315+ "http/429-requested-delay" , slot_retry_after );
2316+ return -1 ;
2317+ }
2318+ return slot_retry_after ;
2319+ } else {
2320+ /* No Retry-After header provided, use configured default */
2321+ if (http_retry_after > http_max_retry_time ) {
2322+ error (_ ("configured http.retryAfter exceeds http.maxRetryTime (%ld > %ld seconds)" ),
2323+ http_retry_after , http_max_retry_time );
2324+ trace2_data_string ("http" , the_repository ,
2325+ "http/429-error" , "config-exceeds-max-retry-time" );
2326+ return -1 ;
2327+ }
2328+ trace2_data_string ("http" , the_repository ,
2329+ "http/429-retry-source" , "config-default" );
2330+ return http_retry_after ;
2331+ }
2332+ }
2333+
2334+ static int http_request_recoverable (const char * url ,
22572335 void * result , int target ,
22582336 struct http_get_options * options )
22592337{
22602338 int i = 3 ;
22612339 int ret ;
2340+ int rate_limit_retries = http_max_retries ;
22622341
22632342 if (always_auth_proactively ())
22642343 credential_fill (the_repository , & http_auth , 1 );
22652344
22662345 ret = http_request (url , result , target , options );
22672346
2268- if (ret != HTTP_OK && ret != HTTP_REAUTH )
2347+ if (ret != HTTP_OK && ret != HTTP_REAUTH && ret != HTTP_RATE_LIMITED )
22692348 return ret ;
22702349
2271- if (options && options -> effective_url && options -> base_url ) {
2350+ /* If retries are disabled and we got a 429, fail immediately */
2351+ if (ret == HTTP_RATE_LIMITED && !http_max_retries )
2352+ return HTTP_ERROR ;
2353+
2354+ if (options -> effective_url && options -> base_url ) {
22722355 if (update_url_from_redirect (options -> base_url ,
22732356 url , options -> effective_url )) {
22742357 credential_from_url (& http_auth , options -> base_url -> buf );
22752358 url = options -> effective_url -> buf ;
22762359 }
22772360 }
22782361
2279- while (ret == HTTP_REAUTH && -- i ) {
2362+ while ((ret == HTTP_REAUTH && -- i ) ||
2363+ (ret == HTTP_RATE_LIMITED && -- rate_limit_retries )) {
2364+ long retry_delay = -1 ;
22802365 /*
22812366 * The previous request may have put cruft into our output stream; we
22822367 * should clear it out before making our next request.
@@ -2301,19 +2386,36 @@ static int http_request_reauth(const char *url,
23012386 default :
23022387 BUG ("Unknown http_request target" );
23032388 }
2304-
2305- credential_fill (the_repository , & http_auth , 1 );
2389+ if (ret == HTTP_RATE_LIMITED ) {
2390+ retry_delay = handle_rate_limit_retry (options -> retry_after );
2391+ if (retry_delay < 0 )
2392+ return HTTP_ERROR ;
2393+
2394+ if (retry_delay > 0 ) {
2395+ warning (_ ("rate limited, waiting %ld seconds before retry" ), retry_delay );
2396+ trace2_data_intmax ("http" , the_repository ,
2397+ "http/retry-sleep-seconds" , retry_delay );
2398+ sleep (retry_delay );
2399+ }
2400+ } else if (ret == HTTP_REAUTH ) {
2401+ credential_fill (the_repository , & http_auth , 1 );
2402+ }
23062403
23072404 ret = http_request (url , result , target , options );
23082405 }
2406+ if (ret == HTTP_RATE_LIMITED ) {
2407+ trace2_data_string ("http" , the_repository ,
2408+ "http/429-error" , "retries-exhausted" );
2409+ return HTTP_RATE_LIMITED ;
2410+ }
23092411 return ret ;
23102412}
23112413
23122414int http_get_strbuf (const char * url ,
23132415 struct strbuf * result ,
23142416 struct http_get_options * options )
23152417{
2316- return http_request_reauth (url , result , HTTP_REQUEST_STRBUF , options );
2418+ return http_request_recoverable (url , result , HTTP_REQUEST_STRBUF , options );
23172419}
23182420
23192421/*
@@ -2337,7 +2439,7 @@ int http_get_file(const char *url, const char *filename,
23372439 goto cleanup ;
23382440 }
23392441
2340- ret = http_request_reauth (url , result , HTTP_REQUEST_FILE , options );
2442+ ret = http_request_recoverable (url , result , HTTP_REQUEST_FILE , options );
23412443 fclose (result );
23422444
23432445 if (ret == HTTP_OK && finalize_object_file (the_repository , tmpfile .buf , filename ))
0 commit comments