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,40 @@ 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 = strtol (buf .buf , & endptr , 10 );
280+
281+ /* Check if it's a valid integer (delay-seconds format) */
282+ if (endptr != buf .buf && * endptr == '\0' && retry_after > 0 ) {
283+ slot -> results -> retry_after = retry_after ;
284+ } else {
285+ /* Try parsing as HTTP-date format */
286+ timestamp_t timestamp ;
287+ int offset ;
288+ if (!parse_date_basic (buf .buf , & timestamp , & offset )) {
289+ /* Successfully parsed as date, calculate delay from now */
290+ timestamp_t now = time (NULL );
291+ if (timestamp > now ) {
292+ slot -> results -> retry_after = (long )(timestamp - now );
293+ } else {
294+ /* Past date means retry immediately */
295+ slot -> results -> retry_after = 0 ;
296+ }
297+ }
298+ }
299+ }
300+
301+ http_auth .header_is_last_match = 1 ;
302+ goto exit ;
303+ }
304+
260305 /*
261306 * This line could be a continuation of the previously matched header
262307 * field. If this is the case then we should append this value to the
@@ -575,6 +620,21 @@ static int http_options(const char *var, const char *value,
575620 return 0 ;
576621 }
577622
623+ if (!strcmp ("http.retryafter" , var )) {
624+ http_retry_after = git_config_int (var , value , ctx -> kvi );
625+ return 0 ;
626+ }
627+
628+ if (!strcmp ("http.maxretries" , var )) {
629+ http_max_retries = git_config_int (var , value , ctx -> kvi );
630+ return 0 ;
631+ }
632+
633+ if (!strcmp ("http.maxretrytime" , var )) {
634+ http_max_retry_time = git_config_int (var , value , ctx -> kvi );
635+ return 0 ;
636+ }
637+
578638 /* Fall back on the default ones */
579639 return git_default_config (var , value , ctx , data );
580640}
@@ -1422,6 +1482,10 @@ void http_init(struct remote *remote, const char *url, int proactive_auth)
14221482 set_long_from_env (& curl_tcp_keepintvl , "GIT_TCP_KEEPINTVL" );
14231483 set_long_from_env (& curl_tcp_keepcnt , "GIT_TCP_KEEPCNT" );
14241484
1485+ set_long_from_env (& http_retry_after , "GIT_HTTP_RETRY_AFTER" );
1486+ set_long_from_env (& http_max_retries , "GIT_HTTP_MAX_RETRIES" );
1487+ set_long_from_env (& http_max_retry_time , "GIT_HTTP_MAX_RETRY_TIME" );
1488+
14251489 curl_default = get_curl_handle ();
14261490}
14271491
@@ -1871,6 +1935,12 @@ static int handle_curl_result(struct slot_results *results)
18711935 }
18721936 return HTTP_REAUTH ;
18731937 }
1938+ } else if (results -> http_code == 429 ) {
1939+ /* Store the retry_after value for use in retry logic */
1940+ last_retry_after = results -> retry_after ;
1941+ trace2_data_intmax ("http" , the_repository , "http/429-retry-after" ,
1942+ last_retry_after );
1943+ return HTTP_RATE_LIMITED ;
18741944 } else {
18751945 if (results -> http_connectcode == 407 )
18761946 credential_reject (the_repository , & proxy_auth );
@@ -1886,6 +1956,8 @@ int run_one_slot(struct active_request_slot *slot,
18861956 struct slot_results * results )
18871957{
18881958 slot -> results = results ;
1959+ /* Initialize retry_after to -1 (not set) */
1960+ results -> retry_after = -1 ;
18891961 if (!start_active_slot (slot )) {
18901962 xsnprintf (curl_errorstr , sizeof (curl_errorstr ),
18911963 "failed to start HTTP request" );
@@ -2149,6 +2221,7 @@ static int http_request(const char *url,
21492221 }
21502222
21512223 curl_easy_setopt (slot -> curl , CURLOPT_HEADERFUNCTION , fwrite_wwwauth );
2224+ curl_easy_setopt (slot -> curl , CURLOPT_HEADERDATA , slot );
21522225
21532226 accept_language = http_get_accept_language_header ();
21542227
@@ -2253,19 +2326,35 @@ static int update_url_from_redirect(struct strbuf *base,
22532326 return 1 ;
22542327}
22552328
2329+ /*
2330+ * Sleep for the specified number of seconds before retrying.
2331+ */
2332+ static void sleep_for_retry (long retry_after )
2333+ {
2334+ if (retry_after > 0 ) {
2335+ fprintf (stderr , _ ("Rate limited, waiting %ld seconds before retry...\n" ), retry_after );
2336+ trace2_region_enter ("http" , "retry-sleep" , the_repository );
2337+ trace2_data_intmax ("http" , the_repository , "http/retry-sleep-seconds" ,
2338+ retry_after );
2339+ sleep (retry_after );
2340+ trace2_region_leave ("http" , "retry-sleep" , the_repository );
2341+ }
2342+ }
2343+
22562344static int http_request_reauth (const char * url ,
22572345 void * result , int target ,
22582346 struct http_get_options * options )
22592347{
22602348 int i = 3 ;
22612349 int ret ;
2350+ int rate_limit_retries = http_max_retries ;
22622351
22632352 if (always_auth_proactively ())
22642353 credential_fill (the_repository , & http_auth , 1 );
22652354
22662355 ret = http_request (url , result , target , options );
22672356
2268- if (ret != HTTP_OK && ret != HTTP_REAUTH )
2357+ if (ret != HTTP_OK && ret != HTTP_REAUTH && ret != HTTP_RATE_LIMITED )
22692358 return ret ;
22702359
22712360 if (options && options -> effective_url && options -> base_url ) {
@@ -2276,7 +2365,7 @@ static int http_request_reauth(const char *url,
22762365 }
22772366 }
22782367
2279- while (ret == HTTP_REAUTH && -- i ) {
2368+ while (( ret == HTTP_REAUTH || ret == HTTP_RATE_LIMITED ) && -- i ) {
22802369 /*
22812370 * The previous request may have put cruft into our output stream; we
22822371 * should clear it out before making our next request.
@@ -2302,7 +2391,69 @@ static int http_request_reauth(const char *url,
23022391 BUG ("Unknown http_request target" );
23032392 }
23042393
2305- credential_fill (the_repository , & http_auth , 1 );
2394+ if (ret == HTTP_RATE_LIMITED ) {
2395+ /* Handle rate limiting with retry logic */
2396+ int retry_attempt = http_max_retries - rate_limit_retries + 1 ;
2397+
2398+ trace2_data_intmax ("http" , the_repository , "http/429-retry-attempt" ,
2399+ retry_attempt );
2400+
2401+ if (rate_limit_retries <= 0 ) {
2402+ /* Retries are disabled or exhausted */
2403+ if (http_max_retries > 0 ) {
2404+ error (_ ("Too many rate limit retries, giving up" ));
2405+ trace2_data_string ("http" , the_repository ,
2406+ "http/429-error" , "retries-exhausted" );
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+ trace2_data_string ("http" , the_repository ,
2422+ "http/429-error" , "exceeds-max-retry-time" );
2423+ trace2_data_intmax ("http" , the_repository ,
2424+ "http/429-requested-delay" , last_retry_after );
2425+ last_retry_after = -1 ; /* Reset after use */
2426+ return HTTP_ERROR ;
2427+ }
2428+ sleep_for_retry (last_retry_after );
2429+ last_retry_after = -1 ; /* Reset after use */
2430+ } else {
2431+ /* No Retry-After header provided */
2432+ if (http_retry_after < 0 ) {
2433+ /* Not configured - exit with error */
2434+ error (_ ("Rate limited (HTTP 429) and no Retry-After header provided. "
2435+ "Configure http.retryAfter or set GIT_HTTP_RETRY_AFTER." ));
2436+ trace2_data_string ("http" , the_repository ,
2437+ "http/429-error" , "no-retry-after-config" );
2438+ return HTTP_ERROR ;
2439+ }
2440+ /* Check if configured default exceeds maximum allowed */
2441+ if (http_retry_after > http_max_retry_time ) {
2442+ error (_ ("Configured http.retryAfter (%ld seconds) exceeds "
2443+ "http.maxRetryTime (%ld seconds)" ),
2444+ http_retry_after , http_max_retry_time );
2445+ trace2_data_string ("http" , the_repository ,
2446+ "http/429-error" , "config-exceeds-max-retry-time" );
2447+ return HTTP_ERROR ;
2448+ }
2449+ /* Use configured default retry-after value */
2450+ trace2_data_string ("http" , the_repository ,
2451+ "http/429-retry-source" , "config-default" );
2452+ sleep_for_retry (http_retry_after );
2453+ }
2454+ } else if (ret == HTTP_REAUTH ) {
2455+ credential_fill (the_repository , & http_auth , 1 );
2456+ }
23062457
23072458 ret = http_request (url , result , target , options );
23082459 }
0 commit comments