diff --git a/docs/libcurl/curl_easy_setopt.md b/docs/libcurl/curl_easy_setopt.md index ccca56de6b..0646ea5dc9 100644 --- a/docs/libcurl/curl_easy_setopt.md +++ b/docs/libcurl/curl_easy_setopt.md @@ -332,6 +332,12 @@ Callback for wildcard matching. See CURLOPT_FNMATCH_FUNCTION(3) Follow HTTP redirects. See CURLOPT_FOLLOWLOCATION(3) +## CURLOPT_FORBID_RETRY_ON_REUSE + +Prevent curl from transparently retry a transfer on a new connection when a +previously reused connection was reset or otherwise no longer unavailable. +See CURLOPT_FORBID_RETRY_ON_REUSE(3) + ## CURLOPT_FORBID_REUSE Prevent subsequent connections from reusing this. See CURLOPT_FORBID_REUSE(3) diff --git a/docs/libcurl/opts/CURLOPT_FORBID_RETRY_ON_REUSE.md b/docs/libcurl/opts/CURLOPT_FORBID_RETRY_ON_REUSE.md new file mode 100644 index 0000000000..40fb220ea2 --- /dev/null +++ b/docs/libcurl/opts/CURLOPT_FORBID_RETRY_ON_REUSE.md @@ -0,0 +1,106 @@ +--- +c: Copyright (C) 2025 Hewlett Packard Enterprise Development LP +SPDX-License-Identifier: curl +Title: CURLOPT_FORBID_RETRY_ON_REUSE +Section: 3 +Source: libcurl +See-also: + - CURLOPT_FRESH_CONNECT (3) + - CURLOPT_FORBID_REUSE (3) +Protocol: + - All +Added-in: 8.16.0 +--- + +# NAME + +CURLOPT_FORBID_RETRY_ON_REUSE - prevent retry in case of reused connection is reset + +# SYNOPSIS + +~~~c +#include + +CURLcode curl_easy_setopt(CURL *handle, CURLOPT_FORBID_RETRY_ON_REUSE, + long forbid_retry); +~~~ + +# DESCRIPTION + +Pass a long. Set *forbid_retry* to 1 to prevent libcurl from resubmitting a +transfer on a new connection when the previous reused connection was reset. +Instead, the reset is returned to the caller as a `CURLE_RECV_ERROR`. + +This option only affects retry decision if the connection used to perform +the request (and from which CURL received a TCP reset) is reused. + +Normally, libcurl considers connection resets on reused connections as a +transient timing issue. The reset event is only visible to curl at the time +libcurl attempts to issue transfer on that connection. As a result, +sometimes libcurl may issue more than 1 identical transfers in a row on +different connections with a single `curl_easy_perform()` call. Caller is +not informed about any previous transfer attempts that may or may not have +arrived at the server before the reset happened. + +This can break transactional application-level protocols if the protocol +state machine considers connection state changes as part of state +transition edges, or the protocol involves non-idempotent requests with +side effects. + +Before introduction of this option, the only way to avoid unobservable +retries was to set CURLOPT_FORBID_REUSE(3) to 1. However, without +connection reuse and keepalive, the application pays significant +overhead from the TCP and TLS handshake for every transfer. This option +decouples implicit retry behavior from connection reuse, allowing the +application to benefit from connection reuse without risking unobservable +retries. + +Set to 0 to have libcurl transparently retry the transfer on a new +connection if the reused connection was reset (default behavior). + +# DEFAULT + +0 + +# %PROTOCOLS% + +# EXAMPLE + +~~~c +#include + +int main(void) +{ + CURL *curl = curl_easy_init(); + if(curl) { + curl_easy_setopt(curl, CURLOPT_URL, "https://example.com/"); + curl_easy_setopt(curl, CURLOPT_FORBID_RETRY_ON_REUSE, 1L); + + /* + * This request will establish a connection and retained it for reuse + * after the transfer is done. + */ + curl_easy_perform(curl); + + /* Wait long enough for the server to drop connection. */ + sleep(60); + + /* + * curl will fail this request from the reset instead of + * transmitting the same transfer again over a new connection. + */ + curl_easy_perform(curl); + + curl_easy_cleanup(curl); + } +} +~~~ + +# %AVAILABILITY% + +# RETURN VALUE + +curl_easy_setopt(3) returns a CURLcode indicating success or error. + +CURLE_OK (0) means everything was OK, non-zero means an error occurred, see +libcurl-errors(3). diff --git a/docs/libcurl/opts/Makefile.inc b/docs/libcurl/opts/Makefile.inc index 98691cac3c..66b9d1b773 100644 --- a/docs/libcurl/opts/Makefile.inc +++ b/docs/libcurl/opts/Makefile.inc @@ -185,6 +185,7 @@ man_MANS = \ CURLOPT_FNMATCH_DATA.3 \ CURLOPT_FNMATCH_FUNCTION.3 \ CURLOPT_FOLLOWLOCATION.3 \ + CURLOPT_FORBID_RETRY_ON_REUSE.3 \ CURLOPT_FORBID_REUSE.3 \ CURLOPT_FRESH_CONNECT.3 \ CURLOPT_FTP_ACCOUNT.3 \ diff --git a/docs/libcurl/symbols-in-versions b/docs/libcurl/symbols-in-versions index bfcf357bf6..c22b48abff 100644 --- a/docs/libcurl/symbols-in-versions +++ b/docs/libcurl/symbols-in-versions @@ -646,6 +646,7 @@ CURLOPT_FILETIME 7.5 CURLOPT_FNMATCH_DATA 7.21.0 CURLOPT_FNMATCH_FUNCTION 7.21.0 CURLOPT_FOLLOWLOCATION 7.1 +CURLOPT_FORBID_RETRY_ON_REUSE 8.16.0 CURLOPT_FORBID_REUSE 7.7 CURLOPT_FRESH_CONNECT 7.7 CURLOPT_FTP_ACCOUNT 7.13.0 diff --git a/include/curl/curl.h b/include/curl/curl.h index 6f4aa90f13..f5cffa40e6 100644 --- a/include/curl/curl.h +++ b/include/curl/curl.h @@ -2258,6 +2258,10 @@ typedef enum { /* set TLS supported signature algorithms */ CURLOPT(CURLOPT_SSL_SIGNATURE_ALGORITHMS, CURLOPTTYPE_STRINGPOINT, 328), + /* prevent transparent retries on a new connection when a previously + * reused connection was reset or otherwise no longer unavailable. */ + CURLOPT(CURLOPT_FORBID_RETRY_ON_REUSE, CURLOPTTYPE_LONG, 329), + CURLOPT_LASTENTRY /* the last unused */ } CURLoption; diff --git a/lib/easyoptions.c b/lib/easyoptions.c index 03d676df0e..26443ad3cc 100644 --- a/lib/easyoptions.c +++ b/lib/easyoptions.c @@ -97,6 +97,7 @@ const struct curl_easyoption Curl_easyopts[] = { {"FNMATCH_DATA", CURLOPT_FNMATCH_DATA, CURLOT_CBPTR, 0}, {"FNMATCH_FUNCTION", CURLOPT_FNMATCH_FUNCTION, CURLOT_FUNCTION, 0}, {"FOLLOWLOCATION", CURLOPT_FOLLOWLOCATION, CURLOT_LONG, 0}, + {"FORBID_RETRY_ON_REUSE", CURLOPT_FORBID_RETRY_ON_REUSE, CURLOT_LONG, 0}, {"FORBID_REUSE", CURLOPT_FORBID_REUSE, CURLOT_LONG, 0}, {"FRESH_CONNECT", CURLOPT_FRESH_CONNECT, CURLOT_LONG, 0}, {"FTPAPPEND", CURLOPT_APPEND, CURLOT_LONG, CURLOT_FLAG_ALIAS}, @@ -380,6 +381,6 @@ const struct curl_easyoption Curl_easyopts[] = { */ int Curl_easyopts_check(void) { - return (CURLOPT_LASTENTRY % 10000) != (328 + 1); + return (CURLOPT_LASTENTRY % 10000) != (329 + 1); } #endif diff --git a/lib/setopt.c b/lib/setopt.c index 1684177382..386cff7785 100644 --- a/lib/setopt.c +++ b/lib/setopt.c @@ -452,6 +452,13 @@ static CURLcode setopt_bool(struct Curl_easy *data, CURLoption option, */ s->reuse_forbid = enabled; break; + case CURLOPT_FORBID_RETRY_ON_REUSE: + /** + * No retry under any circumstances even when a reset happens on a reused + * connection. + */ + s->retry_on_reuse_forbid = enabled; + break; case CURLOPT_FRESH_CONNECT: /* * This transfer shall not use a previously cached connection but diff --git a/lib/transfer.c b/lib/transfer.c index 50f621056f..5859d9d81c 100644 --- a/lib/transfer.c +++ b/lib/transfer.c @@ -667,7 +667,7 @@ CURLcode Curl_retry_request(struct Curl_easy *data, char **url) return CURLE_OK; if((data->req.bytecount + data->req.headerbytecount == 0) && - conn->bits.reuse && + conn->bits.reuse && conn->bits.retry_on_reuse && (!data->req.no_body || (conn->handler->protocol & PROTO_FAMILY_HTTP)) #ifndef CURL_DISABLE_RTSP && (data->set.rtspreq != RTSPREQ_RECEIVE) diff --git a/lib/url.c b/lib/url.c index 20161cac44..a266d07b09 100644 --- a/lib/url.c +++ b/lib/url.c @@ -3514,6 +3514,10 @@ static CURLcode create_conn(struct Curl_easy *data, *************************************************************/ if((conn->given->flags&PROTOPT_SSL) && conn->bits.httpproxy) conn->bits.tunnel_proxy = TRUE; + + /* Carry over the no-retry-on-reuse into connection bits. */ + conn->bits.retry_on_reuse = !data->set.retry_on_reuse_forbid; + #endif /************************************************************* diff --git a/lib/urldata.h b/lib/urldata.h index a35512cf7a..76db6ce99e 100644 --- a/lib/urldata.h +++ b/lib/urldata.h @@ -379,6 +379,8 @@ struct ConnectBits { /* always modify bits.close with the connclose() and connkeep() macros! */ BIT(close); /* if set, we close the connection after this request */ BIT(reuse); /* if set, this is a reused connection */ + BIT(retry_on_reuse); /* if set, a reused connection after connection reset + is allowed to retry the request */ BIT(altused); /* this is an alt-svc "redirect" */ BIT(conn_to_host); /* if set, this connection has a "connect to host" that overrides the host in the URL */ @@ -1605,6 +1607,7 @@ struct UserDefined { #endif BIT(reuse_forbid); /* forbidden to be reused, close after use */ BIT(reuse_fresh); /* do not reuse an existing connection */ + BIT(retry_on_reuse_forbid); /* do not retry under connection reset */ BIT(no_signal); /* do not use any signal/alarm handler */ BIT(tcp_nodelay); /* whether to enable TCP_NODELAY or not */ BIT(ignorecl); /* ignore content length */ diff --git a/src/tool_operate.c b/src/tool_operate.c index 57d9e9d3bd..9706b5bbbd 100644 --- a/src/tool_operate.c +++ b/src/tool_operate.c @@ -1398,6 +1398,8 @@ static CURLcode add_parallel_transfers(CURLM *multi, CURLSH *share, #ifdef DEBUGBUILD if(getenv("CURL_FORBID_REUSE")) (void)curl_easy_setopt(per->curl, CURLOPT_FORBID_REUSE, 1L); + if(getenv("CURL_FORBID_RETRY_ON_REUSE")) + (void)curl_easy_setopt(per->curl, CURLOPT_FORBID_RETRY_ON_REUSE, 1L); #endif mcode = curl_multi_add_handle(multi, per->curl); @@ -1868,6 +1870,8 @@ static CURLcode serial_transfers(CURLSH *share) #ifdef DEBUGBUILD if(getenv("CURL_FORBID_REUSE")) (void)curl_easy_setopt(per->curl, CURLOPT_FORBID_REUSE, 1L); + if(getenv("CURL_FORBID_RETRY_ON_REUSE")) + (void)curl_easy_setopt(per->curl, CURLOPT_FORBID_RETRY_ON_REUSE, 1L); if(global->test_duphandle) { CURL *dup = curl_easy_duphandle(per->curl);