http: expect 100 rework

Move all handling of HTTP's `Expect: 100-continue` feature into a client
reader. Add sending flag `KEEP_SEND_TIMED` that triggers transfer
sending on general events like a timer.

HTTP installs a `CURL_CR_PROTOCOL` reader when announcing `Expect:
100-continue`. That reader works as follows:

- on first invocation, records time, starts the `EXPIRE_100_TIMEOUT`
  timer, disables `KEEP_SEND`, enables `KEEP_SEND_TIMER` and returns 0,
  eos=FALSE like a paused upload.

- on subsequent invocation it checks if the timer has expired. If so, it
  enables `KEEP_SEND` and switches to passing through reads to the
  underlying readers.

Transfer handling's `readwrite()` will be invoked when a timer expires
(like `EXPIRE_100_TIMEOUT`) or when data from the server arrives. Seeing
`KEEP_SEND_TIMER`, it will try to upload more data, which triggers
reading from the client readers again. Which then may lead to a new
pausing or cause the upload to start.

Flags and timestamps connected to this have been moved from
`SingleRequest` into the reader's context.

Closes #13110
This commit is contained in:
Stefan Eissing 2024-03-11 17:23:15 +01:00 committed by Daniel Stenberg
parent 3d0fd382a2
commit 80a3b830cc
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
17 changed files with 301 additions and 189 deletions

View file

@ -102,6 +102,9 @@
*/
static bool http_should_fail(struct Curl_easy *data);
static bool http_exp100_is_waiting(struct Curl_easy *data);
static CURLcode http_exp100_add_reader(struct Curl_easy *data);
static void http_exp100_send_anyway(struct Curl_easy *data);
/*
* HTTP handler interface.
@ -1275,31 +1278,6 @@ static const char *get_http_string(const struct Curl_easy *data,
}
#endif
/* check and possibly add an Expect: header */
static CURLcode expect100(struct Curl_easy *data, struct dynbuf *req)
{
CURLcode result = CURLE_OK;
if(!data->state.disableexpect &&
Curl_use_http_1_1plus(data, data->conn) &&
(data->conn->httpversion < 20)) {
/* if not doing HTTP 1.0 or version 2, or disabled explicitly, we add an
Expect: 100-continue to the headers which actually speeds up post
operations (as there is one packet coming back from the web server) */
const char *ptr = Curl_checkheaders(data, STRCONST("Expect"));
if(ptr) {
data->state.expect100header =
Curl_compareheader(ptr, STRCONST("Expect:"), STRCONST("100-continue"));
}
else {
result = Curl_dyn_addn(req, STRCONST("Expect: 100-continue\r\n"));
if(!result)
data->state.expect100header = TRUE;
}
}
return result;
}
enum proxy_use {
HEADER_SERVER, /* direct to server */
HEADER_PROXY, /* regular request to proxy */
@ -2164,26 +2142,39 @@ CURLcode Curl_http_req_set_reader(struct Curl_easy *data,
return result;
}
static CURLcode addexpect(struct Curl_easy *data, struct dynbuf *r)
static CURLcode addexpect(struct Curl_easy *data, struct dynbuf *r,
bool *announced_exp100)
{
data->state.expect100header = FALSE;
CURLcode result;
char *ptr;
*announced_exp100 = FALSE;
/* Avoid Expect: 100-continue if Upgrade: is used */
if(data->req.upgr101 == UPGR101_INIT) {
/* For really small puts we don't use Expect: headers at all, and for
the somewhat bigger ones we allow the app to disable it. Just make
sure that the expect100header is always set to the preferred value
here. */
char *ptr = Curl_checkheaders(data, STRCONST("Expect"));
if(ptr) {
data->state.expect100header =
Curl_compareheader(ptr, STRCONST("Expect:"),
STRCONST("100-continue"));
if(data->req.upgr101 != UPGR101_INIT)
return CURLE_OK;
/* For really small puts we don't use Expect: headers at all, and for
the somewhat bigger ones we allow the app to disable it. Just make
sure that the expect100header is always set to the preferred value
here. */
ptr = Curl_checkheaders(data, STRCONST("Expect"));
if(ptr) {
*announced_exp100 =
Curl_compareheader(ptr, STRCONST("Expect:"), STRCONST("100-continue"));
}
else if(!data->state.disableexpect &&
Curl_use_http_1_1plus(data, data->conn) &&
(data->conn->httpversion < 20)) {
/* if not doing HTTP 1.0 or version 2, or disabled explicitly, we add an
Expect: 100-continue to the headers which actually speeds up post
operations (as there is one packet coming back from the web server) */
curl_off_t client_len = Curl_creader_client_length(data);
if(client_len > EXPECT_100_THRESHOLD || client_len < 0) {
result = Curl_dyn_addn(r, STRCONST("Expect: 100-continue\r\n"));
if(result)
return result;
*announced_exp100 = TRUE;
}
else {
curl_off_t client_len = Curl_creader_client_length(data);
if(client_len > EXPECT_100_THRESHOLD || client_len < 0)
return expect100(data, r);
}
}
return CURLE_OK;
}
@ -2193,6 +2184,7 @@ CURLcode Curl_http_req_complete(struct Curl_easy *data,
{
CURLcode result = CURLE_OK;
curl_off_t req_clen;
bool announced_exp100 = FALSE;
DEBUGASSERT(data->conn);
#ifndef USE_HYPER
@ -2251,7 +2243,7 @@ CURLcode Curl_http_req_complete(struct Curl_easy *data,
goto out;
}
}
result = addexpect(data, r);
result = addexpect(data, r, &announced_exp100);
if(result)
goto out;
break;
@ -2262,6 +2254,8 @@ CURLcode Curl_http_req_complete(struct Curl_easy *data,
/* end of headers */
result = Curl_dyn_addn(r, STRCONST("\r\n"));
Curl_pgrsSetUploadSize(data, req_clen);
if(announced_exp100)
result = http_exp100_add_reader(data);
out:
if(!result) {
@ -3434,11 +3428,7 @@ static CURLcode http_rw_headers(struct Curl_easy *data,
k->headerline = 0; /* restart the header line counter */
/* if we did wait for this do enable write now! */
if(k->exp100 > EXP100_SEND_DATA) {
k->exp100 = EXP100_SEND_DATA;
k->keepon |= KEEP_SEND;
Curl_expire_done(data, EXPIRE_100_TIMEOUT);
}
Curl_http_exp100_got100(data);
break;
case 101:
if(conn->httpversion == 11) {
@ -3617,13 +3607,11 @@ static CURLcode http_rw_headers(struct Curl_easy *data,
* request body has been sent we stop sending and mark the
* connection for closure after we've read the entire response.
*/
Curl_expire_done(data, EXPIRE_100_TIMEOUT);
if(!Curl_req_done_sending(data)) {
if((k->httpcode == 417) && data->state.expect100header) {
if((k->httpcode == 417) && Curl_http_exp100_is_selected(data)) {
/* 417 Expectation Failed - try again without the Expect
header */
if(!k->writebytecount &&
k->exp100 == EXP100_AWAITING_CONTINUE) {
if(!k->writebytecount && http_exp100_is_waiting(data)) {
infof(data, "Got HTTP failure 417 while waiting for a 100");
}
else {
@ -3641,10 +3629,7 @@ static CURLcode http_rw_headers(struct Curl_easy *data,
}
else if(data->set.http_keep_sending_on_error) {
infof(data, "HTTP error before end of send, keep sending");
if(k->exp100 > EXP100_SEND_DATA) {
k->exp100 = EXP100_SEND_DATA;
k->keepon |= KEEP_SEND;
}
http_exp100_send_anyway(data);
}
else {
infof(data, "HTTP error before end of send, stop sending");
@ -3652,8 +3637,6 @@ static CURLcode http_rw_headers(struct Curl_easy *data,
result = Curl_req_abort_sending(data);
if(result)
return result;
if(data->state.expect100header)
k->exp100 = EXP100_FAILED;
}
}
break;
@ -4337,4 +4320,142 @@ void Curl_http_resp_free(struct http_resp *resp)
}
}
struct cr_exp100_ctx {
struct Curl_creader super;
struct curltime start; /* time started waiting */
enum expect100 state;
};
/* Expect: 100-continue client reader, blocking uploads */
static void http_exp100_continue(struct Curl_easy *data,
struct Curl_creader *reader)
{
struct cr_exp100_ctx *ctx = reader->ctx;
if(ctx->state > EXP100_SEND_DATA) {
ctx->state = EXP100_SEND_DATA;
data->req.keepon |= KEEP_SEND;
data->req.keepon &= ~KEEP_SEND_TIMED;
Curl_expire_done(data, EXPIRE_100_TIMEOUT);
}
}
static CURLcode cr_exp100_read(struct Curl_easy *data,
struct Curl_creader *reader,
char *buf, size_t blen,
size_t *nread, bool *eos)
{
struct cr_exp100_ctx *ctx = reader->ctx;
timediff_t ms;
switch(ctx->state) {
case EXP100_SENDING_REQUEST:
/* We are now waiting for a reply from the server or
* a timeout on our side */
DEBUGF(infof(data, "cr_exp100_read, start AWAITING_CONTINUE"));
ctx->state = EXP100_AWAITING_CONTINUE;
ctx->start = Curl_now();
Curl_expire(data, data->set.expect_100_timeout, EXPIRE_100_TIMEOUT);
data->req.keepon &= ~KEEP_SEND;
data->req.keepon |= KEEP_SEND_TIMED;
*nread = 0;
*eos = FALSE;
return CURLE_OK;
case EXP100_FAILED:
DEBUGF(infof(data, "cr_exp100_read, expectation failed, error"));
*nread = 0;
*eos = FALSE;
return CURLE_READ_ERROR;
case EXP100_AWAITING_CONTINUE:
ms = Curl_timediff(Curl_now(), ctx->start);
if(ms < data->set.expect_100_timeout) {
DEBUGF(infof(data, "cr_exp100_read, AWAITING_CONTINUE, not expired"));
data->req.keepon &= ~KEEP_SEND;
data->req.keepon |= KEEP_SEND_TIMED;
*nread = 0;
*eos = FALSE;
return CURLE_OK;
}
/* we've waited long enough, continue anyway */
http_exp100_continue(data, reader);
infof(data, "Done waiting for 100-continue");
FALLTHROUGH();
default:
DEBUGF(infof(data, "cr_exp100_read, pass through"));
return Curl_creader_read(data, reader->next, buf, blen, nread, eos);
}
}
static void cr_exp100_done(struct Curl_easy *data,
struct Curl_creader *reader, int premature)
{
struct cr_exp100_ctx *ctx = reader->ctx;
ctx->state = premature? EXP100_FAILED : EXP100_SEND_DATA;
data->req.keepon &= ~KEEP_SEND_TIMED;
Curl_expire_done(data, EXPIRE_100_TIMEOUT);
}
static const struct Curl_crtype cr_exp100 = {
"cr-exp100",
Curl_creader_def_init,
cr_exp100_read,
Curl_creader_def_close,
Curl_creader_def_needs_rewind,
Curl_creader_def_total_length,
Curl_creader_def_resume_from,
Curl_creader_def_rewind,
Curl_creader_def_unpause,
cr_exp100_done,
sizeof(struct cr_exp100_ctx)
};
static CURLcode http_exp100_add_reader(struct Curl_easy *data)
{
struct Curl_creader *reader = NULL;
CURLcode result;
result = Curl_creader_create(&reader, data, &cr_exp100,
CURL_CR_PROTOCOL);
if(!result)
result = Curl_creader_add(data, reader);
if(!result) {
struct cr_exp100_ctx *ctx = reader->ctx;
ctx->state = EXP100_SENDING_REQUEST;
}
if(result && reader)
Curl_creader_free(data, reader);
return result;
}
void Curl_http_exp100_got100(struct Curl_easy *data)
{
struct Curl_creader *r = Curl_creader_get_by_type(data, &cr_exp100);
if(r)
http_exp100_continue(data, r);
}
static bool http_exp100_is_waiting(struct Curl_easy *data)
{
struct Curl_creader *r = Curl_creader_get_by_type(data, &cr_exp100);
if(r) {
struct cr_exp100_ctx *ctx = r->ctx;
return (ctx->state == EXP100_AWAITING_CONTINUE);
}
return FALSE;
}
static void http_exp100_send_anyway(struct Curl_easy *data)
{
struct Curl_creader *r = Curl_creader_get_by_type(data, &cr_exp100);
if(r)
http_exp100_continue(data, r);
}
bool Curl_http_exp100_is_selected(struct Curl_easy *data)
{
struct Curl_creader *r = Curl_creader_get_by_type(data, &cr_exp100);
return r? TRUE : FALSE;
}
#endif /* CURL_DISABLE_HTTP */