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,10 +2150,10 @@ 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{
21242155 struct active_request_slot * slot ;
2125- struct slot_results results ;
2156+ struct slot_results results = { . retry_after = -1 } ;
21262157 struct curl_slist * headers = http_copy_default_headers ();
21272158 struct strbuf buf = STRBUF_INIT ;
21282159 const char * accept_language ;
@@ -2156,22 +2187,19 @@ static int http_request(const char *url,
21562187 headers = curl_slist_append (headers , accept_language );
21572188
21582189 strbuf_addstr (& buf , "Pragma:" );
2159- if (options && options -> no_cache )
2190+ if (options -> no_cache )
21602191 strbuf_addstr (& buf , " no-cache" );
2161- if (options && options -> initial_request &&
2192+ if (options -> initial_request &&
21622193 http_follow_config == HTTP_FOLLOW_INITIAL )
21632194 curl_easy_setopt (slot -> curl , CURLOPT_FOLLOWLOCATION , 1L );
21642195
21652196 headers = curl_slist_append (headers , buf .buf );
21662197
21672198 /* Add additional headers here */
2168- if (options && options -> extra_headers ) {
2199+ if (options -> extra_headers ) {
21692200 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- }
2201+ for_each_string_list_item (item , options -> extra_headers )
2202+ headers = curl_slist_append (headers , item -> string );
21752203 }
21762204
21772205 headers = http_append_auth_header (& http_auth , headers );
@@ -2183,15 +2211,26 @@ static int http_request(const char *url,
21832211
21842212 ret = run_one_slot (slot , & results );
21852213
2186- if (options && options -> content_type ) {
2214+ #ifdef GIT_CURL_HAVE_CURLINFO_RETRY_AFTER
2215+ if (ret == HTTP_RATE_LIMITED ) {
2216+ curl_off_t retry_after ;
2217+ if (curl_easy_getinfo (slot -> curl , CURLINFO_RETRY_AFTER ,
2218+ & retry_after ) == CURLE_OK && retry_after > 0 )
2219+ results .retry_after = (long )retry_after ;
2220+ }
2221+ #endif
2222+
2223+ options -> retry_after = results .retry_after ;
2224+
2225+ if (options -> content_type ) {
21872226 struct strbuf raw = STRBUF_INIT ;
21882227 curlinfo_strbuf (slot -> curl , CURLINFO_CONTENT_TYPE , & raw );
21892228 extract_content_type (& raw , options -> content_type ,
21902229 options -> charset );
21912230 strbuf_release (& raw );
21922231 }
21932232
2194- if (options && options -> effective_url )
2233+ if (options -> effective_url )
21952234 curlinfo_strbuf (slot -> curl , CURLINFO_EFFECTIVE_URL ,
21962235 options -> effective_url );
21972236
@@ -2253,30 +2292,76 @@ static int update_url_from_redirect(struct strbuf *base,
22532292 return 1 ;
22542293}
22552294
2256- static int http_request_reauth (const char * url ,
2295+ /*
2296+ * Compute the retry delay for an HTTP 429 response.
2297+ * Returns a negative value if configuration is invalid (delay exceeds
2298+ * http.maxRetryTime), otherwise returns the delay in seconds (>= 0).
2299+ */
2300+ static long handle_rate_limit_retry (long slot_retry_after )
2301+ {
2302+ /* Use the slot-specific retry_after value or configured default */
2303+ if (slot_retry_after >= 0 ) {
2304+ /* Check if retry delay exceeds maximum allowed */
2305+ if (slot_retry_after > http_max_retry_time ) {
2306+ error (_ ("response requested a delay greater than http.maxRetryTime (%ld > %ld seconds)" ),
2307+ slot_retry_after , http_max_retry_time );
2308+ trace2_data_string ("http" , the_repository ,
2309+ "http/429-error" , "exceeds-max-retry-time" );
2310+ trace2_data_intmax ("http" , the_repository ,
2311+ "http/429-requested-delay" , slot_retry_after );
2312+ return -1 ;
2313+ }
2314+ return slot_retry_after ;
2315+ } else {
2316+ /* No Retry-After header provided, use configured default */
2317+ if (http_retry_after > http_max_retry_time ) {
2318+ error (_ ("configured http.retryAfter exceeds http.maxRetryTime (%ld > %ld seconds)" ),
2319+ http_retry_after , http_max_retry_time );
2320+ trace2_data_string ("http" , the_repository ,
2321+ "http/429-error" , "config-exceeds-max-retry-time" );
2322+ return -1 ;
2323+ }
2324+ trace2_data_string ("http" , the_repository ,
2325+ "http/429-retry-source" , "config-default" );
2326+ return http_retry_after ;
2327+ }
2328+ }
2329+
2330+ static int http_request_recoverable (const char * url ,
22572331 void * result , int target ,
22582332 struct http_get_options * options )
22592333{
2334+ static struct http_get_options empty_opts ;
22602335 int i = 3 ;
22612336 int ret ;
2337+ int rate_limit_retries = http_max_retries ;
2338+
2339+ if (!options )
2340+ options = & empty_opts ;
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