Merge branch 'vp/http-rate-limit-retries' into seen

The HTTP transport learned to react to "429 Too Many Requests".

* vp/http-rate-limit-retries:
  http: add support for HTTP 429 rate limit retries
  remote-curl: introduce show_http_message_fatal() helper
  strbuf_attach: fix call sites to pass correct alloc
  strbuf: pass correct alloc to strbuf_attach() in strbuf_reencode()
This commit is contained in:
Junio C Hamano
2026-03-09 16:20:03 -07:00
16 changed files with 622 additions and 34 deletions

188
http.c
View File

@@ -22,6 +22,8 @@
#include "object-file.h"
#include "odb.h"
#include "tempfile.h"
#include "date.h"
#include "trace2.h"
static struct trace_key trace_curl = TRACE_KEY_INIT(CURL);
static int trace_curl_data = 1;
@@ -149,6 +151,11 @@ static char *cached_accept_language;
static char *http_ssl_backend;
static int http_schannel_check_revoke = 1;
static long http_retry_after = 0;
static long http_max_retries = 0;
static long http_max_retry_time = 300;
/*
* With the backend being set to `schannel`, setting sslCAinfo would override
* 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)
return size && (*ptr == ' ' || *ptr == '\t');
}
static size_t fwrite_wwwauth(char *ptr, size_t eltsize, size_t nmemb, void *p UNUSED)
static size_t fwrite_headers(char *ptr, size_t eltsize, size_t nmemb, void *p MAYBE_UNUSED)
{
size_t size = eltsize * nmemb;
struct strvec *values = &http_auth.wwwauth_headers;
@@ -257,6 +264,50 @@ static size_t fwrite_wwwauth(char *ptr, size_t eltsize, size_t nmemb, void *p UN
goto exit;
}
#ifndef GIT_CURL_HAVE_CURLINFO_RETRY_AFTER
/* Parse Retry-After header for rate limiting (for curl < 7.66.0) */
if (skip_iprefix_mem(ptr, size, "retry-after:", &val, &val_len)) {
struct active_request_slot *slot = (struct active_request_slot *)p;
strbuf_add(&buf, val, val_len);
strbuf_trim(&buf);
if (slot && slot->results) {
/* Parse the retry-after value (delay-seconds or HTTP-date) */
char *endptr;
long retry_after;
errno = 0;
retry_after = strtol(buf.buf, &endptr, 10);
/* Check if it's a valid integer (delay-seconds format) */
if (endptr != buf.buf && *endptr == '\0' &&
errno != ERANGE && retry_after >= 0) {
slot->results->retry_after = retry_after;
} else {
/* Try parsing as HTTP-date format */
timestamp_t timestamp;
int offset;
if (!parse_date_basic(buf.buf, &timestamp, &offset)) {
/* Successfully parsed as date, calculate delay from now */
timestamp_t now = time(NULL);
if (timestamp > now) {
slot->results->retry_after = (long)(timestamp - now);
} else {
/* Past date means retry immediately */
slot->results->retry_after = 0;
}
} else {
/* Failed to parse as either delay-seconds or HTTP-date */
warning(_("unable to parse Retry-After header value: '%s'"), buf.buf);
}
}
}
goto exit;
}
#endif
/*
* This line could be a continuation of the previously matched header
* field. If this is the case then we should append this value to the
@@ -342,6 +393,17 @@ static void finish_active_slot(struct active_request_slot *slot)
curl_easy_getinfo(slot->curl, CURLINFO_HTTP_CONNECTCODE,
&slot->results->http_connectcode);
#ifdef GIT_CURL_HAVE_CURLINFO_RETRY_AFTER
if (slot->results->http_code == 429) {
curl_off_t retry_after;
CURLcode res = curl_easy_getinfo(slot->curl,
CURLINFO_RETRY_AFTER,
&retry_after);
if (res == CURLE_OK && retry_after > 0)
slot->results->retry_after = (long)retry_after;
}
#endif
}
/* Run callback if appropriate */
@@ -575,6 +637,21 @@ static int http_options(const char *var, const char *value,
return 0;
}
if (!strcmp("http.retryafter", var)) {
http_retry_after = git_config_int(var, value, ctx->kvi);
return 0;
}
if (!strcmp("http.maxretries", var)) {
http_max_retries = git_config_int(var, value, ctx->kvi);
return 0;
}
if (!strcmp("http.maxretrytime", var)) {
http_max_retry_time = git_config_int(var, value, ctx->kvi);
return 0;
}
/* Fall back on the default ones */
return git_default_config(var, value, ctx, data);
}
@@ -1422,6 +1499,10 @@ void http_init(struct remote *remote, const char *url, int proactive_auth)
set_long_from_env(&curl_tcp_keepintvl, "GIT_TCP_KEEPINTVL");
set_long_from_env(&curl_tcp_keepcnt, "GIT_TCP_KEEPCNT");
set_long_from_env(&http_retry_after, "GIT_HTTP_RETRY_AFTER");
set_long_from_env(&http_max_retries, "GIT_HTTP_MAX_RETRIES");
set_long_from_env(&http_max_retry_time, "GIT_HTTP_MAX_RETRY_TIME");
curl_default = get_curl_handle();
}
@@ -1871,6 +1952,10 @@ static int handle_curl_result(struct slot_results *results)
}
return HTTP_REAUTH;
}
} else if (results->http_code == 429) {
trace2_data_intmax("http", the_repository, "http/429-retry-after",
results->retry_after);
return HTTP_RATE_LIMITED;
} else {
if (results->http_connectcode == 407)
credential_reject(the_repository, &proxy_auth);
@@ -1886,6 +1971,9 @@ int run_one_slot(struct active_request_slot *slot,
struct slot_results *results)
{
slot->results = results;
/* Initialize retry_after to -1 (not set) */
results->retry_after = -1;
if (!start_active_slot(slot)) {
xsnprintf(curl_errorstr, sizeof(curl_errorstr),
"failed to start HTTP request");
@@ -2119,7 +2207,8 @@ static void http_opt_request_remainder(CURL *curl, off_t pos)
static int http_request(const char *url,
void *result, int target,
const struct http_get_options *options)
const struct http_get_options *options,
long *retry_after_out)
{
struct active_request_slot *slot;
struct slot_results results;
@@ -2148,7 +2237,8 @@ static int http_request(const char *url,
fwrite_buffer);
}
curl_easy_setopt(slot->curl, CURLOPT_HEADERFUNCTION, fwrite_wwwauth);
curl_easy_setopt(slot->curl, CURLOPT_HEADERFUNCTION, fwrite_headers);
curl_easy_setopt(slot->curl, CURLOPT_HEADERDATA, slot);
accept_language = http_get_accept_language_header();
@@ -2183,6 +2273,10 @@ static int http_request(const char *url,
ret = run_one_slot(slot, &results);
/* Store retry_after from slot results if output parameter provided */
if (retry_after_out)
*retry_after_out = results.retry_after;
if (options && options->content_type) {
struct strbuf raw = STRBUF_INIT;
curlinfo_strbuf(slot->curl, CURLINFO_CONTENT_TYPE, &raw);
@@ -2253,21 +2347,79 @@ static int update_url_from_redirect(struct strbuf *base,
return 1;
}
static int http_request_reauth(const char *url,
/*
* Handle rate limiting retry logic for HTTP 429 responses.
* Returns a negative value if retries are exhausted or configuration is invalid,
* otherwise returns the delay value (>= 0) to indicate the retry should proceed.
*/
static long handle_rate_limit_retry(int *rate_limit_retries, long slot_retry_after)
{
int retry_attempt = http_max_retries - *rate_limit_retries + 1;
trace2_data_intmax("http", the_repository, "http/429-retry-attempt",
retry_attempt);
if (*rate_limit_retries <= 0) {
/* Retries are disabled or exhausted */
if (http_max_retries > 0) {
error(_("too many rate limit retries, giving up"));
trace2_data_string("http", the_repository,
"http/429-error", "retries-exhausted");
}
return -1;
}
(*rate_limit_retries)--;
/* Use the slot-specific retry_after value or configured default */
if (slot_retry_after >= 0) {
/* Check if retry delay exceeds maximum allowed */
if (slot_retry_after > http_max_retry_time) {
error(_("response requested a delay greater than http.maxRetryTime (%ld > %ld seconds)"),
slot_retry_after, http_max_retry_time);
trace2_data_string("http", the_repository,
"http/429-error", "exceeds-max-retry-time");
trace2_data_intmax("http", the_repository,
"http/429-requested-delay", slot_retry_after);
return -1;
}
return slot_retry_after;
} else {
/* No Retry-After header provided, use configured default */
if (http_retry_after > http_max_retry_time) {
error(_("configured http.retryAfter exceeds http.maxRetryTime (%ld > %ld seconds)"),
http_retry_after, http_max_retry_time);
trace2_data_string("http", the_repository,
"http/429-error", "config-exceeds-max-retry-time");
return -1;
}
trace2_data_string("http", the_repository,
"http/429-retry-source", "config-default");
return http_retry_after;
}
}
static int http_request_recoverable(const char *url,
void *result, int target,
struct http_get_options *options)
{
int i = 3;
int ret;
int rate_limit_retries = http_max_retries;
long slot_retry_after = -1; /* Per-slot retry_after value */
if (always_auth_proactively())
credential_fill(the_repository, &http_auth, 1);
ret = http_request(url, result, target, options);
ret = http_request(url, result, target, options, &slot_retry_after);
if (ret != HTTP_OK && ret != HTTP_REAUTH)
if (ret != HTTP_OK && ret != HTTP_REAUTH && ret != HTTP_RATE_LIMITED)
return ret;
/* If retries are disabled and we got a 429, fail immediately */
if (ret == HTTP_RATE_LIMITED && !http_max_retries)
return HTTP_ERROR;
if (options && options->effective_url && options->base_url) {
if (update_url_from_redirect(options->base_url,
url, options->effective_url)) {
@@ -2276,7 +2428,8 @@ static int http_request_reauth(const char *url,
}
}
while (ret == HTTP_REAUTH && --i) {
while ((ret == HTTP_REAUTH || ret == HTTP_RATE_LIMITED) && --i) {
long retry_delay = -1;
/*
* The previous request may have put cruft into our output stream; we
* should clear it out before making our next request.
@@ -2301,10 +2454,23 @@ static int http_request_reauth(const char *url,
default:
BUG("Unknown http_request target");
}
if (ret == HTTP_RATE_LIMITED) {
retry_delay = handle_rate_limit_retry(&rate_limit_retries, slot_retry_after);
if (retry_delay < 0)
return HTTP_ERROR;
credential_fill(the_repository, &http_auth, 1);
if (retry_delay > 0) {
warning(_("rate limited, waiting %ld seconds before retry"), retry_delay);
trace2_data_intmax("http", the_repository,
"http/retry-sleep-seconds", retry_delay);
sleep(retry_delay);
}
slot_retry_after = -1; /* Reset after use */
} else if (ret == HTTP_REAUTH) {
credential_fill(the_repository, &http_auth, 1);
}
ret = http_request(url, result, target, options);
ret = http_request(url, result, target, options, &slot_retry_after);
}
return ret;
}
@@ -2313,7 +2479,7 @@ int http_get_strbuf(const char *url,
struct strbuf *result,
struct http_get_options *options)
{
return http_request_reauth(url, result, HTTP_REQUEST_STRBUF, options);
return http_request_recoverable(url, result, HTTP_REQUEST_STRBUF, options);
}
/*
@@ -2337,7 +2503,7 @@ int http_get_file(const char *url, const char *filename,
goto cleanup;
}
ret = http_request_reauth(url, result, HTTP_REQUEST_FILE, options);
ret = http_request_recoverable(url, result, HTTP_REQUEST_FILE, options);
fclose(result);
if (ret == HTTP_OK && finalize_object_file(the_repository, tmpfile.buf, filename))