22
#include "object-file.h"
23
#include "odb.h"
24
#include "tempfile.h"
25
+#include "date.h"
26
+#include "trace2.h"
27
28
static struct trace_key trace_curl = TRACE_KEY_INIT(CURL);
29
static int trace_curl_data = 1;
151
static char *http_ssl_backend;
152
153
static int http_schannel_check_revoke = 1;
154
+
155
+static long http_retry_after = 0;
156
+static long http_max_retries = 0;
157
+static long http_max_retry_time = 300;
158
+
159
/*
160
* With the backend being set to `schannel`, setting sslCAinfo would override
161
* the Certificate Store in cURL v7.60.0 and later, which is not what we want
216
return size && (*ptr == ' ' || *ptr == '\t');
217
}
218
212
-static size_t fwrite_wwwauth(char *ptr, size_t eltsize, size_t nmemb, void *p UNUSED)
219
+static size_t fwrite_wwwauth(char *ptr, size_t eltsize, size_t nmemb, void *p MAYBE_UNUSED)
220
{
221
size_t size = eltsize * nmemb;
222
struct strvec *values = &http_auth.wwwauth_headers;
582
return 0;
583
}
584
585
+ if (!strcmp("http.retryafter", var)) {
586
+ http_retry_after = git_config_int(var, value, ctx->kvi);
587
+ return 0;
588
+ }
589
+
590
+ if (!strcmp("http.maxretries", var)) {
591
+ http_max_retries = git_config_int(var, value, ctx->kvi);
592
+ return 0;
593
+ }
594
+
595
+ if (!strcmp("http.maxretrytime", var)) {
596
+ http_max_retry_time = git_config_int(var, value, ctx->kvi);
597
+ return 0;
598
+ }
599
+
600
/* Fall back on the default ones */
601
return git_default_config(var, value, ctx, data);
602
}
1444
set_long_from_env(&curl_tcp_keepintvl, "GIT_TCP_KEEPINTVL");
1445
set_long_from_env(&curl_tcp_keepcnt, "GIT_TCP_KEEPCNT");
1446
1447
+ set_long_from_env(&http_retry_after, "GIT_HTTP_RETRY_AFTER");
1448
+ set_long_from_env(&http_max_retries, "GIT_HTTP_MAX_RETRIES");
1449
+ set_long_from_env(&http_max_retry_time, "GIT_HTTP_MAX_RETRY_TIME");
1450
+
1451
curl_default = get_curl_handle();
1452
}
1453
1897
}
1898
return HTTP_REAUTH;
1899
}
1900
+ } else if (results->http_code == 429) {
1901
+ trace2_data_intmax("http", the_repository, "http/429-retry-after",
1902
+ results->retry_after);
1903
+ return HTTP_RATE_LIMITED;
1904
} else {
1905
if (results->http_connectcode == 407)
1906
credential_reject(the_repository, &proxy_auth);
1916
struct slot_results *results)
1917
{
1918
slot->results = results;
1919
+
1920
if (!start_active_slot(slot)) {
1921
xsnprintf(curl_errorstr, sizeof(curl_errorstr),
1922
"failed to start HTTP request");
2150
2151
static int http_request(const char *url,
2152
void *result, int target,
2122
- const struct http_get_options *options)
2153
+ struct http_get_options *options)
2154
{
2155
struct active_request_slot *slot;
2125
- struct slot_results results;
2156
+ struct slot_results results = { .retry_after = -1 };
2157
struct curl_slist *headers = http_copy_default_headers();
2158
struct strbuf buf = STRBUF_INIT;
2159
const char *accept_language;
2187
headers = curl_slist_append(headers, accept_language);
2188
2189
strbuf_addstr(&buf, "Pragma:");
2159
- if (options && options->no_cache)
2190
+ if (options->no_cache)
2191
strbuf_addstr(&buf, " no-cache");
2161
- if (options && options->initial_request &&
2192
+ if (options->initial_request &&
2193
http_follow_config == HTTP_FOLLOW_INITIAL)
2194
curl_easy_setopt(slot->curl, CURLOPT_FOLLOWLOCATION, 1L);
2195
2196
headers = curl_slist_append(headers, buf.buf);
2197
2198
/* Add additional headers here */
2168
- if (options && options->extra_headers) {
2199
+ if (options->extra_headers) {
2200
const struct string_list_item *item;
2170
- if (options && options->extra_headers) {
2171
- for_each_string_list_item(item, options->extra_headers) {
2172
- headers = curl_slist_append(headers, item->string);
2173
- }
2174
- }
2201
+ for_each_string_list_item(item, options->extra_headers)
2202
+ headers = curl_slist_append(headers, item->string);
2203
}
2204
2205
headers = http_append_auth_header(&http_auth, headers);
2211
2212
ret = run_one_slot(slot, &results);
2213
2186
- if (options && options->content_type) {
2214
+#ifdef GIT_CURL_HAVE_CURLINFO_RETRY_AFTER
2215
+ if (ret == HTTP_RATE_LIMITED) {
2216
+ curl_off_t retry_after;
2217
+ if (curl_easy_getinfo(slot->curl, CURLINFO_RETRY_AFTER,
2218
+ &retry_after) == CURLE_OK && retry_after > 0)
2219
+ results.retry_after = (long)retry_after;
2220
+ }
2221
+#endif
2222
+
2223
+ options->retry_after = results.retry_after;
2224
+
2225
+ if (options->content_type) {
2226
struct strbuf raw = STRBUF_INIT;
2227
curlinfo_strbuf(slot->curl, CURLINFO_CONTENT_TYPE, &raw);
2228
extract_content_type(&raw, options->content_type,
2230
strbuf_release(&raw);
2231
}
2232
2194
- if (options && options->effective_url)
2233
+ if (options->effective_url)
2234
curlinfo_strbuf(slot->curl, CURLINFO_EFFECTIVE_URL,
2235
options->effective_url);
2236
2292
return 1;
2293
}
2294
2256
-static int http_request_reauth(const char *url,
2295
+/*
2296
+ * Compute the retry delay for an HTTP 429 response.
2297
+ * Returns a negative value if configuration is invalid (delay exceeds
2298
+ * http.maxRetryTime), otherwise returns the delay in seconds (>= 0).
2299
+ */
2300
+static long handle_rate_limit_retry(long slot_retry_after)
2301
+{
2302
+ /* Use the slot-specific retry_after value or configured default */
2303
+ if (slot_retry_after >= 0) {
2304
+ /* Check if retry delay exceeds maximum allowed */
2305
+ if (slot_retry_after > http_max_retry_time) {
2306
+ error(_("response requested a delay greater than http.maxRetryTime (%ld > %ld seconds)"),
2307
+ slot_retry_after, http_max_retry_time);
2308
+ trace2_data_string("http", the_repository,
2309
+ "http/429-error", "exceeds-max-retry-time");
2310
+ trace2_data_intmax("http", the_repository,
2311
+ "http/429-requested-delay", slot_retry_after);
2312
+ return -1;
2313
+ }
2314
+ return slot_retry_after;
2315
+ } else {
2316
+ /* No Retry-After header provided, use configured default */
2317
+ if (http_retry_after > http_max_retry_time) {
2318
+ error(_("configured http.retryAfter exceeds http.maxRetryTime (%ld > %ld seconds)"),
2319
+ http_retry_after, http_max_retry_time);
2320
+ trace2_data_string("http", the_repository,
2321
+ "http/429-error", "config-exceeds-max-retry-time");
2322
+ return -1;
2323
+ }
2324
+ trace2_data_string("http", the_repository,
2325
+ "http/429-retry-source", "config-default");
2326
+ return http_retry_after;
2327
+ }
2328
+}
2329
+
2330
+static int http_request_recoverable(const char *url,
2331
void *result, int target,
2332
struct http_get_options *options)
2333
{
2334
+ static struct http_get_options empty_opts;
2335
int i = 3;
2336
int ret;
2337
+ int rate_limit_retries = http_max_retries;
2338
+
2339
+ if (!options)
2340
+ options = &empty_opts;
2341
2342
if (always_auth_proactively())
2343
credential_fill(the_repository, &http_auth, 1);
2344
2345
ret = http_request(url, result, target, options);
2346
2268
- if (ret != HTTP_OK && ret != HTTP_REAUTH)
2347
+ if (ret != HTTP_OK && ret != HTTP_REAUTH && ret != HTTP_RATE_LIMITED)
2348
return ret;
2349
2271
- if (options && options->effective_url && options->base_url) {
2350
+ /* If retries are disabled and we got a 429, fail immediately */
2351
+ if (ret == HTTP_RATE_LIMITED && !http_max_retries)
2352
+ return HTTP_ERROR;
2353
+
2354
+ if (options->effective_url && options->base_url) {
2355
if (update_url_from_redirect(options->base_url,
2356
url, options->effective_url)) {
2357
credential_from_url(&http_auth, options->base_url->buf);
2359
}
2360
}
2361
2279
- while (ret == HTTP_REAUTH && --i) {
2362
+ while ((ret == HTTP_REAUTH && --i) ||
2363
+ (ret == HTTP_RATE_LIMITED && --rate_limit_retries)) {
2364
+ long retry_delay = -1;
2365
/*
2366
* The previous request may have put cruft into our output stream; we
2367
* should clear it out before making our next request.
2386
default:
2387
BUG("Unknown http_request target");
2388
}
2304
-
2305
- credential_fill(the_repository, &http_auth, 1);
2389
+ if (ret == HTTP_RATE_LIMITED) {
2390
+ retry_delay = handle_rate_limit_retry(options->retry_after);
2391
+ if (retry_delay < 0)
2392
+ return HTTP_ERROR;
2393
+
2394
+ if (retry_delay > 0) {
2395
+ warning(_("rate limited, waiting %ld seconds before retry"), retry_delay);
2396
+ trace2_data_intmax("http", the_repository,
2397
+ "http/retry-sleep-seconds", retry_delay);
2398
+ sleep(retry_delay);
2399
+ }
2400
+ } else if (ret == HTTP_REAUTH) {
2401
+ credential_fill(the_repository, &http_auth, 1);
2402
+ }
2403
2404
ret = http_request(url, result, target, options);
2405
}
2406
+ if (ret == HTTP_RATE_LIMITED) {
2407
+ trace2_data_string("http", the_repository,
2408
+ "http/429-error", "retries-exhausted");
2409
+ return HTTP_RATE_LIMITED;
2410
+ }
2411
return ret;
2412
}
2413
2415
struct strbuf *result,
2416
struct http_get_options *options)
2417
{
2316
- return http_request_reauth(url, result, HTTP_REQUEST_STRBUF, options);
2418
+ return http_request_recoverable(url, result, HTTP_REQUEST_STRBUF, options);
2419
}
2420
2421
/*
2439
goto cleanup;
2440
}
2441
2340
- ret = http_request_reauth(url, result, HTTP_REQUEST_FILE, options);
2442
+ ret = http_request_recoverable(url, result, HTTP_REQUEST_FILE, options);
2443
fclose(result);
2444
2445
if (ret == HTTP_OK && finalize_object_file(the_repository, tmpfile.buf, filename))