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,14 @@ static char *cached_accept_language;
149151static char * http_ssl_backend ;
150152
151153static int http_schannel_check_revoke = 1 ;
154+
155+ /* Retry configuration */
156+ static long http_retry_after = -1 ; /* Default retry-after in seconds when header is missing (-1 means not set, exit with 128) */
157+ static long http_max_retries = 0 ; /* Maximum number of retry attempts (0 means retries are disabled) */
158+ static long http_max_retry_time = 300 ; /* Maximum time to wait for a single retry (default 5 minutes) */
159+
160+ /* Store retry_after value from 429 responses for retry logic (-1 = not set, 0 = retry immediately, >0 = delay in seconds) */
161+ static long last_retry_after = -1 ;
152162/*
153163 * With the backend being set to `schannel`, setting sslCAinfo would override
154164 * the Certificate Store in cURL v7.60.0 and later, which is not what we want
@@ -209,13 +219,14 @@ static inline int is_hdr_continuation(const char *ptr, const size_t size)
209219 return size && (* ptr == ' ' || * ptr == '\t' );
210220}
211221
212- static size_t fwrite_wwwauth (char * ptr , size_t eltsize , size_t nmemb , void * p UNUSED )
222+ static size_t fwrite_wwwauth (char * ptr , size_t eltsize , size_t nmemb , void * p )
213223{
214224 size_t size = eltsize * nmemb ;
215225 struct strvec * values = & http_auth .wwwauth_headers ;
216226 struct strbuf buf = STRBUF_INIT ;
217227 const char * val ;
218228 size_t val_len ;
229+ struct active_request_slot * slot = (struct active_request_slot * )p ;
219230
220231 /*
221232 * Header lines may not come NULL-terminated from libcurl so we must
@@ -257,6 +268,47 @@ static size_t fwrite_wwwauth(char *ptr, size_t eltsize, size_t nmemb, void *p UN
257268 goto exit ;
258269 }
259270
271+ /* Parse Retry-After header for rate limiting */
272+ if (skip_iprefix_mem (ptr , size , "retry-after:" , & val , & val_len )) {
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+ http_auth .header_is_last_match = 1 ;
309+ goto exit ;
310+ }
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
@@ -575,6 +627,21 @@ static int http_options(const char *var, const char *value,
575627 return 0 ;
576628 }
577629
630+ if (!strcmp ("http.retryafter" , var )) {
631+ http_retry_after = git_config_int (var , value , ctx -> kvi );
632+ return 0 ;
633+ }
634+
635+ if (!strcmp ("http.maxretries" , var )) {
636+ http_max_retries = git_config_int (var , value , ctx -> kvi );
637+ return 0 ;
638+ }
639+
640+ if (!strcmp ("http.maxretrytime" , var )) {
641+ http_max_retry_time = git_config_int (var , value , ctx -> kvi );
642+ return 0 ;
643+ }
644+
578645 /* Fall back on the default ones */
579646 return git_default_config (var , value , ctx , data );
580647}
@@ -1422,6 +1489,10 @@ void http_init(struct remote *remote, const char *url, int proactive_auth)
14221489 set_long_from_env (& curl_tcp_keepintvl , "GIT_TCP_KEEPINTVL" );
14231490 set_long_from_env (& curl_tcp_keepcnt , "GIT_TCP_KEEPCNT" );
14241491
1492+ set_long_from_env (& http_retry_after , "GIT_HTTP_RETRY_AFTER" );
1493+ set_long_from_env (& http_max_retries , "GIT_HTTP_MAX_RETRIES" );
1494+ set_long_from_env (& http_max_retry_time , "GIT_HTTP_MAX_RETRY_TIME" );
1495+
14251496 curl_default = get_curl_handle ();
14261497}
14271498
@@ -1871,6 +1942,12 @@ static int handle_curl_result(struct slot_results *results)
18711942 }
18721943 return HTTP_REAUTH ;
18731944 }
1945+ } else if (results -> http_code == 429 ) {
1946+ /* Store the retry_after value for use in retry logic */
1947+ last_retry_after = results -> retry_after ;
1948+ trace2_data_intmax ("http" , the_repository , "http/429-retry-after" ,
1949+ last_retry_after );
1950+ return HTTP_RATE_LIMITED ;
18741951 } else {
18751952 if (results -> http_connectcode == 407 )
18761953 credential_reject (the_repository , & proxy_auth );
@@ -1886,6 +1963,8 @@ int run_one_slot(struct active_request_slot *slot,
18861963 struct slot_results * results )
18871964{
18881965 slot -> results = results ;
1966+ /* Initialize retry_after to -1 (not set) */
1967+ results -> retry_after = -1 ;
18891968 if (!start_active_slot (slot )) {
18901969 xsnprintf (curl_errorstr , sizeof (curl_errorstr ),
18911970 "failed to start HTTP request" );
@@ -2149,6 +2228,7 @@ static int http_request(const char *url,
21492228 }
21502229
21512230 curl_easy_setopt (slot -> curl , CURLOPT_HEADERFUNCTION , fwrite_wwwauth );
2231+ curl_easy_setopt (slot -> curl , CURLOPT_HEADERDATA , slot );
21522232
21532233 accept_language = http_get_accept_language_header ();
21542234
@@ -2253,19 +2333,40 @@ static int update_url_from_redirect(struct strbuf *base,
22532333 return 1 ;
22542334}
22552335
2336+ /*
2337+ * Sleep for the specified number of seconds before retrying.
2338+ */
2339+ static void sleep_for_retry (long retry_after )
2340+ {
2341+ if (retry_after > 0 ) {
2342+ unsigned int remaining ;
2343+ warning (_ ("rate limited, waiting %ld seconds before retry" ), retry_after );
2344+ trace2_region_enter ("http" , "retry-sleep" , the_repository );
2345+ trace2_data_intmax ("http" , the_repository , "http/retry-sleep-seconds" ,
2346+ retry_after );
2347+ remaining = sleep (retry_after );
2348+ while (remaining > 0 ) {
2349+ /* Sleep was interrupted, continue sleeping */
2350+ remaining = sleep (remaining );
2351+ }
2352+ trace2_region_leave ("http" , "retry-sleep" , the_repository );
2353+ }
2354+ }
2355+
22562356static int http_request_reauth (const char * url ,
22572357 void * result , int target ,
22582358 struct http_get_options * options )
22592359{
22602360 int i = 3 ;
22612361 int ret ;
2362+ int rate_limit_retries = http_max_retries ;
22622363
22632364 if (always_auth_proactively ())
22642365 credential_fill (the_repository , & http_auth , 1 );
22652366
22662367 ret = http_request (url , result , target , options );
22672368
2268- if (ret != HTTP_OK && ret != HTTP_REAUTH )
2369+ if (ret != HTTP_OK && ret != HTTP_REAUTH && ret != HTTP_RATE_LIMITED )
22692370 return ret ;
22702371
22712372 if (options && options -> effective_url && options -> base_url ) {
@@ -2276,7 +2377,7 @@ static int http_request_reauth(const char *url,
22762377 }
22772378 }
22782379
2279- while (ret == HTTP_REAUTH && -- i ) {
2380+ while (( ret == HTTP_REAUTH || ret == HTTP_RATE_LIMITED ) && -- i ) {
22802381 /*
22812382 * The previous request may have put cruft into our output stream; we
22822383 * should clear it out before making our next request.
@@ -2302,7 +2403,69 @@ static int http_request_reauth(const char *url,
23022403 BUG ("Unknown http_request target" );
23032404 }
23042405
2305- credential_fill (the_repository , & http_auth , 1 );
2406+ if (ret == HTTP_RATE_LIMITED ) {
2407+ /* Handle rate limiting with retry logic */
2408+ int retry_attempt = http_max_retries - rate_limit_retries + 1 ;
2409+
2410+ trace2_data_intmax ("http" , the_repository , "http/429-retry-attempt" ,
2411+ retry_attempt );
2412+
2413+ if (rate_limit_retries <= 0 ) {
2414+ /* Retries are disabled or exhausted */
2415+ if (http_max_retries > 0 ) {
2416+ error (_ ("too many rate limit retries, giving up" ));
2417+ trace2_data_string ("http" , the_repository ,
2418+ "http/429-error" , "retries-exhausted" );
2419+ }
2420+ return HTTP_ERROR ;
2421+ }
2422+
2423+ /* Decrement retries counter */
2424+ rate_limit_retries -- ;
2425+
2426+ /* Use the stored retry_after value or configured default */
2427+ if (last_retry_after >= 0 ) {
2428+ /* Check if retry delay exceeds maximum allowed */
2429+ if (last_retry_after > http_max_retry_time ) {
2430+ error (_ ("rate limited (HTTP 429) requested %ld second delay, "
2431+ "exceeds http.maxRetryTime of %ld seconds" ),
2432+ last_retry_after , http_max_retry_time );
2433+ trace2_data_string ("http" , the_repository ,
2434+ "http/429-error" , "exceeds-max-retry-time" );
2435+ trace2_data_intmax ("http" , the_repository ,
2436+ "http/429-requested-delay" , last_retry_after );
2437+ last_retry_after = -1 ; /* Reset after use */
2438+ return HTTP_ERROR ;
2439+ }
2440+ sleep_for_retry (last_retry_after );
2441+ last_retry_after = -1 ; /* Reset after use */
2442+ } else {
2443+ /* No Retry-After header provided */
2444+ if (http_retry_after < 0 ) {
2445+ /* Not configured - exit with error */
2446+ error (_ ("rate limited (HTTP 429) and no Retry-After header provided. "
2447+ "Configure http.retryAfter or set GIT_HTTP_RETRY_AFTER." ));
2448+ trace2_data_string ("http" , the_repository ,
2449+ "http/429-error" , "no-retry-after-config" );
2450+ return HTTP_ERROR ;
2451+ }
2452+ /* Check if configured default exceeds maximum allowed */
2453+ if (http_retry_after > http_max_retry_time ) {
2454+ error (_ ("configured http.retryAfter (%ld seconds) exceeds "
2455+ "http.maxRetryTime (%ld seconds)" ),
2456+ http_retry_after , http_max_retry_time );
2457+ trace2_data_string ("http" , the_repository ,
2458+ "http/429-error" , "config-exceeds-max-retry-time" );
2459+ return HTTP_ERROR ;
2460+ }
2461+ /* Use configured default retry-after value */
2462+ trace2_data_string ("http" , the_repository ,
2463+ "http/429-retry-source" , "config-default" );
2464+ sleep_for_retry (http_retry_after );
2465+ }
2466+ } else if (ret == HTTP_REAUTH ) {
2467+ credential_fill (the_repository , & http_auth , 1 );
2468+ }
23062469
23072470 ret = http_request (url , result , target , options );
23082471 }
0 commit comments