mirror of
https://github.com/git/git.git
synced 2026-03-11 09:29:49 +01:00
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:
188
http.c
188
http.c
@@ -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, ×tamp, &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))
|
||||
|
||||
Reference in New Issue
Block a user