Compare commits

...

3 Commits

Author SHA1 Message Date
Tianhao Chai
54098af721
Merge f92d9c0c5f into 4a3ed6fc16 2025-08-30 13:45:18 -07:00
Daniel Stenberg
4a3ed6fc16
urlglob: only accept 255 globs
- using {} with single entries makes little sense
- when using {} sets with two entry lists, there can only be 64 to reach
  maximum number of URLs

Verify the max check in test 761
2025-08-30 22:27:28 +02:00
Tianhao Chai
f92d9c0c5f 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.
2025-08-28 02:03:18 -04:00
14 changed files with 179 additions and 6 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);

View File

@ -423,10 +423,13 @@ static CURLcode glob_parse(struct URLGlob *glob, const char *pattern,
if(++glob->size >= glob->palloc) {
struct URLPattern *np = NULL;
glob->palloc *= 2;
if(glob->size < 10000) /* avoid ridiculous amounts */
if(glob->size < 255) { /* avoid ridiculous amounts */
np = realloc(glob->pattern, glob->palloc * sizeof(struct URLPattern));
if(!np)
return globerror(glob, NULL, pos, CURLE_OUT_OF_MEMORY);
if(!np)
return globerror(glob, NULL, pos, CURLE_OUT_OF_MEMORY);
}
else
return globerror(glob, "too many {} sets", pos, CURLE_URL_MALFORMAT);
glob->pattern = np;
}
}

View File

@ -108,7 +108,7 @@ test718 test719 test720 test721 test722 test723 test724 test725 test726 \
test727 test728 test729 test730 test731 test732 test733 test734 test735 \
test736 test737 test738 test739 test740 test741 test742 test743 test744 \
test745 test746 test747 test748 test749 test750 test751 test752 test753 \
test754 test755 test756 test757 test758 test759 test760 \
test754 test755 test756 test757 test758 test759 test760 test761 \
test780 test781 test782 test783 test784 test785 test786 test787 test788 \
test789 test790 test791 test792 test793 test794 test795 test796 test797 \
\

33
tests/data/test761 Normal file
View File

@ -0,0 +1,33 @@
<testcase>
<info>
<keywords>
globbing
</keywords>
</info>
#
# Client-side
<client>
<server>
none
</server>
<name>
too many {} globs
</name>
<command>
http://testingthis/{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b
</command>
</client>
#
# Verify data after the test has been "shot"
<verify>
<errorcode>
3
</errorcode>
<stderr mode="text">
curl: (3) too many {} sets in URL position 403:
http://testingthis/{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a}b{a
</stderr>
</verify>
</testcase>