http: introduce CURLOPT_FORBID_RETRY_ON_REUSE

This patch introduces a curlopt to allow application to request
curl to not retry a request upon a broken reused connection.

Currently libcurl transparently retry a failed request over a new
connection if all of the following are true:

- The previous broken connection was a reused connection.
- The previous broken connection was reset.
- Max retries not exceeded.

This behavior relies the follwing assumptions:
- The connection reset might have occurred before the reused
  connection was picked up for reuse.
- The connection reset only becomes visible because libcurl attempted
  to transmit request over such a connection.

The above assumptions do not always hold in the field. The following
sequence will produce identical externally visible behavior:
- Libcurl transmits request over the connection.
- The peer might have received the request, but the connection cuts off
  right before it could send a response (crash or whatever)
- Libcurl receives connection reset shortly after.

In this case libcurl will choose to establish a new TCP connection and
issue the identical request for a second time.
Given a client side internal state machine implementing a protocol
that requires action upon a connection reset, this behavior has chance
to cause the state machine to miss a state transition and affect
correctness.

This behavior used to be hardwired with no way to disable from users
unless the user explicitly requests `CURLOPT_FORBID_REUSE` and
`CURLOPT_FRESH_CONNECT` at the cost of significant overhead.

The new boolean flag `CURLOPT_FORBID_RETRY_ON_REUSE` decouples retries
from connection reuse, allowing a user to explicitly supress this retry
behavior without losing connection reuse benefits. When the option is set,
a connection reset will be returned as an error to the caller.
The caller assumes responsibility to perform retries according to the
application protocol in this case. The flag defaults to FALSE so that
current behavior remains the same.
This commit is contained in:
Tianhao Chai 2025-08-07 12:33:36 -04:00 committed by Tianhao Chai
parent 3c64ffaff4
commit f92d9c0c5f
11 changed files with 139 additions and 2 deletions

View file

@ -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)

View file

@ -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 <curl/curl.h>
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 <unistd.h>
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).

View file

@ -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 \

View file

@ -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

View file

@ -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;

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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
/*************************************************************

View file

@ -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 */

View file

@ -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);