2222#include "object-file.h"
2323#include "odb.h"
2424#include "tempfile.h"
25+ #include "date.h"
2526
2627static struct trace_key trace_curl = TRACE_KEY_INIT (CURL );
2728static int trace_curl_data = 1 ;
@@ -149,6 +150,14 @@ static char *cached_accept_language;
149150static char * http_ssl_backend ;
150151
151152static int http_schannel_check_revoke = 1 ;
153+
154+ /* Retry configuration */
155+ static long http_retry_after = -1 ; /* Default retry-after in seconds when header is missing (-1 means not set, exit with 128) */
156+ static long http_max_retries = 0 ; /* Maximum number of retry attempts (0 means retries are disabled) */
157+ static long http_max_retry_time = 300 ; /* Maximum time to wait for a single retry (default 5 minutes) */
158+
159+ /* Store retry_after value from 429 responses for retry logic (-1 = not set, 0 = retry immediately, >0 = delay in seconds) */
160+ static long last_retry_after = -1 ;
152161/*
153162 * With the backend being set to `schannel`, setting sslCAinfo would override
154163 * the Certificate Store in cURL v7.60.0 and later, which is not what we want
@@ -209,13 +218,14 @@ static inline int is_hdr_continuation(const char *ptr, const size_t size)
209218 return size && (* ptr == ' ' || * ptr == '\t' );
210219}
211220
212- static size_t fwrite_wwwauth (char * ptr , size_t eltsize , size_t nmemb , void * p UNUSED )
221+ static size_t fwrite_wwwauth (char * ptr , size_t eltsize , size_t nmemb , void * p )
213222{
214223 size_t size = eltsize * nmemb ;
215224 struct strvec * values = & http_auth .wwwauth_headers ;
216225 struct strbuf buf = STRBUF_INIT ;
217226 const char * val ;
218227 size_t val_len ;
228+ struct active_request_slot * slot = (struct active_request_slot * )p ;
219229
220230 /*
221231 * Header lines may not come NULL-terminated from libcurl so we must
@@ -257,6 +267,47 @@ static size_t fwrite_wwwauth(char *ptr, size_t eltsize, size_t nmemb, void *p UN
257267 goto exit ;
258268 }
259269
270+ /* Parse Retry-After header for rate limiting */
271+ if (skip_iprefix_mem (ptr , size , "retry-after:" , & val , & val_len )) {
272+ strbuf_add (& buf , val , val_len );
273+ strbuf_trim (& buf );
274+
275+ if (slot && slot -> results ) {
276+ /* Parse the retry-after value (delay-seconds or HTTP-date) */
277+ char * endptr ;
278+ long retry_after ;
279+
280+ errno = 0 ;
281+ retry_after = strtol (buf .buf , & endptr , 10 );
282+
283+ /* Check if it's a valid integer (delay-seconds format) */
284+ if (endptr != buf .buf && * endptr == '\0' &&
285+ errno != ERANGE && retry_after > 0 ) {
286+ slot -> results -> retry_after = retry_after ;
287+ } else {
288+ /* Try parsing as HTTP-date format */
289+ timestamp_t timestamp ;
290+ int offset ;
291+ if (!parse_date_basic (buf .buf , & timestamp , & offset )) {
292+ /* Successfully parsed as date, calculate delay from now */
293+ timestamp_t now = time (NULL );
294+ if (timestamp > now ) {
295+ slot -> results -> retry_after = (long )(timestamp - now );
296+ } else {
297+ /* Past date means retry immediately */
298+ slot -> results -> retry_after = 0 ;
299+ }
300+ } else {
301+ /* Failed to parse as either delay-seconds or HTTP-date */
302+ warning (_ ("unable to parse Retry-After header value: '%s'" ), buf .buf );
303+ }
304+ }
305+ }
306+
307+ http_auth .header_is_last_match = 1 ;
308+ goto exit ;
309+ }
310+
260311 /*
261312 * This line could be a continuation of the previously matched header
262313 * field. If this is the case then we should append this value to the
@@ -575,6 +626,21 @@ static int http_options(const char *var, const char *value,
575626 return 0 ;
576627 }
577628
629+ if (!strcmp ("http.retryafter" , var )) {
630+ http_retry_after = git_config_int (var , value , ctx -> kvi );
631+ return 0 ;
632+ }
633+
634+ if (!strcmp ("http.maxretries" , var )) {
635+ http_max_retries = git_config_int (var , value , ctx -> kvi );
636+ return 0 ;
637+ }
638+
639+ if (!strcmp ("http.maxretrytime" , var )) {
640+ http_max_retry_time = git_config_int (var , value , ctx -> kvi );
641+ return 0 ;
642+ }
643+
578644 /* Fall back on the default ones */
579645 return git_default_config (var , value , ctx , data );
580646}
@@ -1422,6 +1488,10 @@ void http_init(struct remote *remote, const char *url, int proactive_auth)
14221488 set_long_from_env (& curl_tcp_keepintvl , "GIT_TCP_KEEPINTVL" );
14231489 set_long_from_env (& curl_tcp_keepcnt , "GIT_TCP_KEEPCNT" );
14241490
1491+ set_long_from_env (& http_retry_after , "GIT_HTTP_RETRY_AFTER" );
1492+ set_long_from_env (& http_max_retries , "GIT_HTTP_MAX_RETRIES" );
1493+ set_long_from_env (& http_max_retry_time , "GIT_HTTP_MAX_RETRY_TIME" );
1494+
14251495 curl_default = get_curl_handle ();
14261496}
14271497
@@ -1871,6 +1941,10 @@ static int handle_curl_result(struct slot_results *results)
18711941 }
18721942 return HTTP_REAUTH ;
18731943 }
1944+ } else if (results -> http_code == 429 ) {
1945+ /* Store the retry_after value for use in retry logic */
1946+ last_retry_after = results -> retry_after ;
1947+ return HTTP_RATE_LIMITED ;
18741948 } else {
18751949 if (results -> http_connectcode == 407 )
18761950 credential_reject (the_repository , & proxy_auth );
@@ -1886,6 +1960,8 @@ int run_one_slot(struct active_request_slot *slot,
18861960 struct slot_results * results )
18871961{
18881962 slot -> results = results ;
1963+ /* Initialize retry_after to -1 (not set) */
1964+ results -> retry_after = -1 ;
18891965 if (!start_active_slot (slot )) {
18901966 xsnprintf (curl_errorstr , sizeof (curl_errorstr ),
18911967 "failed to start HTTP request" );
@@ -2149,6 +2225,7 @@ static int http_request(const char *url,
21492225 }
21502226
21512227 curl_easy_setopt (slot -> curl , CURLOPT_HEADERFUNCTION , fwrite_wwwauth );
2228+ curl_easy_setopt (slot -> curl , CURLOPT_HEADERDATA , slot );
21522229
21532230 accept_language = http_get_accept_language_header ();
21542231
@@ -2253,19 +2330,36 @@ static int update_url_from_redirect(struct strbuf *base,
22532330 return 1 ;
22542331}
22552332
2333+ /*
2334+ * Sleep for the specified number of seconds before retrying.
2335+ */
2336+ static void sleep_for_retry (long retry_after )
2337+ {
2338+ if (retry_after > 0 ) {
2339+ unsigned int remaining ;
2340+ warning (_ ("rate limited, waiting %ld seconds before retry" ), retry_after );
2341+ remaining = sleep (retry_after );
2342+ while (remaining > 0 ) {
2343+ /* Sleep was interrupted, continue sleeping */
2344+ remaining = sleep (remaining );
2345+ }
2346+ }
2347+ }
2348+
22562349static int http_request_reauth (const char * url ,
22572350 void * result , int target ,
22582351 struct http_get_options * options )
22592352{
22602353 int i = 3 ;
22612354 int ret ;
2355+ int rate_limit_retries = http_max_retries ;
22622356
22632357 if (always_auth_proactively ())
22642358 credential_fill (the_repository , & http_auth , 1 );
22652359
22662360 ret = http_request (url , result , target , options );
22672361
2268- if (ret != HTTP_OK && ret != HTTP_REAUTH )
2362+ if (ret != HTTP_OK && ret != HTTP_REAUTH && ret != HTTP_RATE_LIMITED )
22692363 return ret ;
22702364
22712365 if (options && options -> effective_url && options -> base_url ) {
@@ -2276,7 +2370,7 @@ static int http_request_reauth(const char *url,
22762370 }
22772371 }
22782372
2279- while (ret == HTTP_REAUTH && -- i ) {
2373+ while (( ret == HTTP_REAUTH || ret == HTTP_RATE_LIMITED ) && -- i ) {
22802374 /*
22812375 * The previous request may have put cruft into our output stream; we
22822376 * should clear it out before making our next request.
@@ -2302,7 +2396,54 @@ static int http_request_reauth(const char *url,
23022396 BUG ("Unknown http_request target" );
23032397 }
23042398
2305- credential_fill (the_repository , & http_auth , 1 );
2399+ if (ret == HTTP_RATE_LIMITED ) {
2400+ /* Handle rate limiting with retry logic */
2401+ int retry_attempt = http_max_retries - rate_limit_retries + 1 ;
2402+
2403+ if (rate_limit_retries <= 0 ) {
2404+ /* Retries are disabled or exhausted */
2405+ if (http_max_retries > 0 ) {
2406+ error (_ ("too many rate limit retries, giving up" ));
2407+ }
2408+ return HTTP_ERROR ;
2409+ }
2410+
2411+ /* Decrement retries counter */
2412+ rate_limit_retries -- ;
2413+
2414+ /* Use the stored retry_after value or configured default */
2415+ if (last_retry_after >= 0 ) {
2416+ /* Check if retry delay exceeds maximum allowed */
2417+ if (last_retry_after > http_max_retry_time ) {
2418+ error (_ ("rate limited (HTTP 429) requested %ld second delay, "
2419+ "exceeds http.maxRetryTime of %ld seconds" ),
2420+ last_retry_after , http_max_retry_time );
2421+ last_retry_after = -1 ; /* Reset after use */
2422+ return HTTP_ERROR ;
2423+ }
2424+ sleep_for_retry (last_retry_after );
2425+ last_retry_after = -1 ; /* Reset after use */
2426+ } else {
2427+ /* No Retry-After header provided */
2428+ if (http_retry_after < 0 ) {
2429+ /* Not configured - exit with error */
2430+ error (_ ("rate limited (HTTP 429) and no Retry-After header provided. "
2431+ "Configure http.retryAfter or set GIT_HTTP_RETRY_AFTER." ));
2432+ return HTTP_ERROR ;
2433+ }
2434+ /* Check if configured default exceeds maximum allowed */
2435+ if (http_retry_after > http_max_retry_time ) {
2436+ error (_ ("configured http.retryAfter (%ld seconds) exceeds "
2437+ "http.maxRetryTime (%ld seconds)" ),
2438+ http_retry_after , http_max_retry_time );
2439+ return HTTP_ERROR ;
2440+ }
2441+ /* Use configured default retry-after value */
2442+ sleep_for_retry (http_retry_after );
2443+ }
2444+ } else if (ret == HTTP_REAUTH ) {
2445+ credential_fill (the_repository , & http_auth , 1 );
2446+ }
23062447
23072448 ret = http_request (url , result , target , options );
23082449 }
0 commit comments