33
44#include "git-compat-util.h"
55#include "git-curl-compat.h"
6+ #include "advice.h"
67#include "environment.h"
78#include "hex.h"
89#include "http.h"
2223#include "object-file.h"
2324#include "odb.h"
2425#include "tempfile.h"
26+ #include "date.h"
27+ #include "trace2.h"
2528
2629static struct trace_key trace_curl = TRACE_KEY_INIT (CURL );
2730static int trace_curl_data = 1 ;
@@ -149,6 +152,11 @@ static char *cached_accept_language;
149152static char * http_ssl_backend ;
150153
151154static int http_schannel_check_revoke = 1 ;
155+
156+ static long http_retry_after = 0 ;
157+ static long http_max_retries = 0 ;
158+ static long http_max_retry_time = 300 ;
159+
152160/*
153161 * With the backend being set to `schannel`, setting sslCAinfo would override
154162 * the Certificate Store in cURL v7.60.0 and later, which is not what we want
@@ -209,7 +217,7 @@ static inline int is_hdr_continuation(const char *ptr, const size_t size)
209217 return size && (* ptr == ' ' || * ptr == '\t' );
210218}
211219
212- static size_t fwrite_wwwauth (char * ptr , size_t eltsize , size_t nmemb , void * p UNUSED )
220+ static size_t fwrite_headers (char * ptr , size_t eltsize , size_t nmemb , void * p MAYBE_UNUSED )
213221{
214222 size_t size = eltsize * nmemb ;
215223 struct strvec * values = & http_auth .wwwauth_headers ;
@@ -257,6 +265,50 @@ static size_t fwrite_wwwauth(char *ptr, size_t eltsize, size_t nmemb, void *p UN
257265 goto exit ;
258266 }
259267
268+ #ifndef GIT_CURL_HAVE_CURLINFO_RETRY_AFTER
269+ /* Parse Retry-After header for rate limiting (for curl < 7.66.0) */
270+ if (skip_iprefix_mem (ptr , size , "retry-after:" , & val , & val_len )) {
271+ struct active_request_slot * slot = (struct active_request_slot * )p ;
272+
273+ strbuf_add (& buf , val , val_len );
274+ strbuf_trim (& buf );
275+
276+ if (slot && slot -> results ) {
277+ /* Parse the retry-after value (delay-seconds or HTTP-date) */
278+ char * endptr ;
279+ long retry_after ;
280+
281+ errno = 0 ;
282+ retry_after = strtol (buf .buf , & endptr , 10 );
283+
284+ /* Check if it's a valid integer (delay-seconds format) */
285+ if (endptr != buf .buf && * endptr == '\0' &&
286+ errno != ERANGE && retry_after >= 0 ) {
287+ slot -> results -> retry_after = retry_after ;
288+ } else {
289+ /* Try parsing as HTTP-date format */
290+ timestamp_t timestamp ;
291+ int offset ;
292+ if (!parse_date_basic (buf .buf , & timestamp , & offset )) {
293+ /* Successfully parsed as date, calculate delay from now */
294+ timestamp_t now = time (NULL );
295+ if (timestamp > now ) {
296+ slot -> results -> retry_after = (long )(timestamp - now );
297+ } else {
298+ /* Past date means retry immediately */
299+ slot -> results -> retry_after = 0 ;
300+ }
301+ } else {
302+ /* Failed to parse as either delay-seconds or HTTP-date */
303+ warning (_ ("unable to parse Retry-After header value: '%s'" ), buf .buf );
304+ }
305+ }
306+ }
307+
308+ goto exit ;
309+ }
310+ #endif
311+
260312 /*
261313 * This line could be a continuation of the previously matched header
262314 * field. If this is the case then we should append this value to the
@@ -342,6 +394,17 @@ static void finish_active_slot(struct active_request_slot *slot)
342394
343395 curl_easy_getinfo (slot -> curl , CURLINFO_HTTP_CONNECTCODE ,
344396 & slot -> results -> http_connectcode );
397+
398+ #ifdef GIT_CURL_HAVE_CURLINFO_RETRY_AFTER
399+ if (slot -> results -> http_code == 429 ) {
400+ curl_off_t retry_after ;
401+ CURLcode res = curl_easy_getinfo (slot -> curl ,
402+ CURLINFO_RETRY_AFTER ,
403+ & retry_after );
404+ if (res == CURLE_OK && retry_after > 0 )
405+ slot -> results -> retry_after = (long )retry_after ;
406+ }
407+ #endif
345408 }
346409
347410 /* Run callback if appropriate */
@@ -575,6 +638,21 @@ static int http_options(const char *var, const char *value,
575638 return 0 ;
576639 }
577640
641+ if (!strcmp ("http.retryafter" , var )) {
642+ http_retry_after = git_config_int (var , value , ctx -> kvi );
643+ return 0 ;
644+ }
645+
646+ if (!strcmp ("http.maxretries" , var )) {
647+ http_max_retries = git_config_int (var , value , ctx -> kvi );
648+ return 0 ;
649+ }
650+
651+ if (!strcmp ("http.maxretrytime" , var )) {
652+ http_max_retry_time = git_config_int (var , value , ctx -> kvi );
653+ return 0 ;
654+ }
655+
578656 /* Fall back on the default ones */
579657 return git_default_config (var , value , ctx , data );
580658}
@@ -1422,6 +1500,10 @@ void http_init(struct remote *remote, const char *url, int proactive_auth)
14221500 set_long_from_env (& curl_tcp_keepintvl , "GIT_TCP_KEEPINTVL" );
14231501 set_long_from_env (& curl_tcp_keepcnt , "GIT_TCP_KEEPCNT" );
14241502
1503+ set_long_from_env (& http_retry_after , "GIT_HTTP_RETRY_AFTER" );
1504+ set_long_from_env (& http_max_retries , "GIT_HTTP_MAX_RETRIES" );
1505+ set_long_from_env (& http_max_retry_time , "GIT_HTTP_MAX_RETRY_TIME" );
1506+
14251507 curl_default = get_curl_handle ();
14261508}
14271509
@@ -1871,6 +1953,10 @@ static int handle_curl_result(struct slot_results *results)
18711953 }
18721954 return HTTP_REAUTH ;
18731955 }
1956+ } else if (results -> http_code == 429 ) {
1957+ trace2_data_intmax ("http" , the_repository , "http/429-retry-after" ,
1958+ results -> retry_after );
1959+ return HTTP_RATE_LIMITED ;
18741960 } else {
18751961 if (results -> http_connectcode == 407 )
18761962 credential_reject (the_repository , & proxy_auth );
@@ -1886,6 +1972,9 @@ int run_one_slot(struct active_request_slot *slot,
18861972 struct slot_results * results )
18871973{
18881974 slot -> results = results ;
1975+ /* Initialize retry_after to -1 (not set) */
1976+ results -> retry_after = -1 ;
1977+
18891978 if (!start_active_slot (slot )) {
18901979 xsnprintf (curl_errorstr , sizeof (curl_errorstr ),
18911980 "failed to start HTTP request" );
@@ -2119,7 +2208,8 @@ static void http_opt_request_remainder(CURL *curl, off_t pos)
21192208
21202209static int http_request (const char * url ,
21212210 void * result , int target ,
2122- const struct http_get_options * options )
2211+ const struct http_get_options * options ,
2212+ long * retry_after_out )
21232213{
21242214 struct active_request_slot * slot ;
21252215 struct slot_results results ;
@@ -2148,7 +2238,8 @@ static int http_request(const char *url,
21482238 fwrite_buffer );
21492239 }
21502240
2151- curl_easy_setopt (slot -> curl , CURLOPT_HEADERFUNCTION , fwrite_wwwauth );
2241+ curl_easy_setopt (slot -> curl , CURLOPT_HEADERFUNCTION , fwrite_headers );
2242+ curl_easy_setopt (slot -> curl , CURLOPT_HEADERDATA , slot );
21522243
21532244 accept_language = http_get_accept_language_header ();
21542245
@@ -2183,6 +2274,10 @@ static int http_request(const char *url,
21832274
21842275 ret = run_one_slot (slot , & results );
21852276
2277+ /* Store retry_after from slot results if output parameter provided */
2278+ if (retry_after_out )
2279+ * retry_after_out = results .retry_after ;
2280+
21862281 if (options && options -> content_type ) {
21872282 struct strbuf raw = STRBUF_INIT ;
21882283 curlinfo_strbuf (slot -> curl , CURLINFO_CONTENT_TYPE , & raw );
@@ -2253,21 +2348,79 @@ static int update_url_from_redirect(struct strbuf *base,
22532348 return 1 ;
22542349}
22552350
2256- static int http_request_reauth (const char * url ,
2351+ /*
2352+ * Handle rate limiting retry logic for HTTP 429 responses.
2353+ * Returns a negative value if retries are exhausted or configuration is invalid,
2354+ * otherwise returns the delay value (>= 0) to indicate the retry should proceed.
2355+ */
2356+ static long handle_rate_limit_retry (int * rate_limit_retries , long slot_retry_after )
2357+ {
2358+ int retry_attempt = http_max_retries - * rate_limit_retries + 1 ;
2359+
2360+ trace2_data_intmax ("http" , the_repository , "http/429-retry-attempt" ,
2361+ retry_attempt );
2362+
2363+ if (* rate_limit_retries <= 0 ) {
2364+ /* Retries are disabled or exhausted */
2365+ if (http_max_retries > 0 ) {
2366+ error (_ ("too many rate limit retries, giving up" ));
2367+ trace2_data_string ("http" , the_repository ,
2368+ "http/429-error" , "retries-exhausted" );
2369+ }
2370+ return -1 ;
2371+ }
2372+
2373+ (* rate_limit_retries )-- ;
2374+
2375+ /* Use the slot-specific retry_after value or configured default */
2376+ if (slot_retry_after >= 0 ) {
2377+ /* Check if retry delay exceeds maximum allowed */
2378+ if (slot_retry_after > http_max_retry_time ) {
2379+ error (_ ("response requested a delay greater than http.maxRetryTime (%ld > %ld seconds)" ),
2380+ slot_retry_after , http_max_retry_time );
2381+ trace2_data_string ("http" , the_repository ,
2382+ "http/429-error" , "exceeds-max-retry-time" );
2383+ trace2_data_intmax ("http" , the_repository ,
2384+ "http/429-requested-delay" , slot_retry_after );
2385+ return -1 ;
2386+ }
2387+ return slot_retry_after ;
2388+ } else {
2389+ /* No Retry-After header provided, use configured default */
2390+ if (http_retry_after > http_max_retry_time ) {
2391+ error (_ ("configured http.retryAfter exceeds http.maxRetryTime (%ld > %ld seconds)" ),
2392+ http_retry_after , http_max_retry_time );
2393+ trace2_data_string ("http" , the_repository ,
2394+ "http/429-error" , "config-exceeds-max-retry-time" );
2395+ return -1 ;
2396+ }
2397+ trace2_data_string ("http" , the_repository ,
2398+ "http/429-retry-source" , "config-default" );
2399+ return http_retry_after ;
2400+ }
2401+ }
2402+
2403+ static int http_request_recoverable (const char * url ,
22572404 void * result , int target ,
22582405 struct http_get_options * options )
22592406{
22602407 int i = 3 ;
22612408 int ret ;
2409+ int rate_limit_retries = http_max_retries ;
2410+ long slot_retry_after = -1 ; /* Per-slot retry_after value */
22622411
22632412 if (always_auth_proactively ())
22642413 credential_fill (the_repository , & http_auth , 1 );
22652414
2266- ret = http_request (url , result , target , options );
2415+ ret = http_request (url , result , target , options , & slot_retry_after );
22672416
2268- if (ret != HTTP_OK && ret != HTTP_REAUTH )
2417+ if (ret != HTTP_OK && ret != HTTP_REAUTH && ret != HTTP_RATE_LIMITED )
22692418 return ret ;
22702419
2420+ /* If retries are disabled and we got a 429, fail immediately */
2421+ if (ret == HTTP_RATE_LIMITED && !http_max_retries )
2422+ return HTTP_ERROR ;
2423+
22712424 if (options && options -> effective_url && options -> base_url ) {
22722425 if (update_url_from_redirect (options -> base_url ,
22732426 url , options -> effective_url )) {
@@ -2276,7 +2429,8 @@ static int http_request_reauth(const char *url,
22762429 }
22772430 }
22782431
2279- while (ret == HTTP_REAUTH && -- i ) {
2432+ while ((ret == HTTP_REAUTH || ret == HTTP_RATE_LIMITED ) && -- i ) {
2433+ long retry_delay = -1 ;
22802434 /*
22812435 * The previous request may have put cruft into our output stream; we
22822436 * should clear it out before making our next request.
@@ -2301,10 +2455,23 @@ static int http_request_reauth(const char *url,
23012455 default :
23022456 BUG ("Unknown http_request target" );
23032457 }
2458+ if (ret == HTTP_RATE_LIMITED ) {
2459+ retry_delay = handle_rate_limit_retry (& rate_limit_retries , slot_retry_after );
2460+ if (retry_delay < 0 )
2461+ return HTTP_ERROR ;
2462+
2463+ if (retry_delay > 0 ) {
2464+ warning (_ ("rate limited, waiting %ld seconds before retry" ), retry_delay );
2465+ trace2_data_intmax ("http" , the_repository ,
2466+ "http/retry-sleep-seconds" , retry_delay );
2467+ sleep (retry_delay );
2468+ }
2469+ slot_retry_after = -1 ; /* Reset after use */
2470+ } else if (ret == HTTP_REAUTH ) {
2471+ credential_fill (the_repository , & http_auth , 1 );
2472+ }
23042473
2305- credential_fill (the_repository , & http_auth , 1 );
2306-
2307- ret = http_request (url , result , target , options );
2474+ ret = http_request (url , result , target , options , & slot_retry_after );
23082475 }
23092476 return ret ;
23102477}
@@ -2313,7 +2480,7 @@ int http_get_strbuf(const char *url,
23132480 struct strbuf * result ,
23142481 struct http_get_options * options )
23152482{
2316- return http_request_reauth (url , result , HTTP_REQUEST_STRBUF , options );
2483+ return http_request_recoverable (url , result , HTTP_REQUEST_STRBUF , options );
23172484}
23182485
23192486/*
@@ -2337,7 +2504,7 @@ int http_get_file(const char *url, const char *filename,
23372504 goto cleanup ;
23382505 }
23392506
2340- ret = http_request_reauth (url , result , HTTP_REQUEST_FILE , options );
2507+ ret = http_request_recoverable (url , result , HTTP_REQUEST_FILE , options );
23412508 fclose (result );
23422509
23432510 if (ret == HTTP_OK && finalize_object_file (the_repository , tmpfile .buf , filename ))
0 commit comments