From 1ec912d2926767478783af950e5f7ce4e9ed2f7b Mon Sep 17 00:00:00 2001 From: Daniel Stenberg Date: Tue, 31 Mar 2026 07:56:45 +0200 Subject: [PATCH] http: refactor http_on_response into sub functions Reduces complexity. Closes #21172 --- lib/http.c | 470 +++++++++++++++++++++++++++++----------------------- lib/http2.h | 1 - 2 files changed, 261 insertions(+), 210 deletions(-) diff --git a/lib/http.c b/lib/http.c index 96e7b0de0c..0f53eaa9a0 100644 --- a/lib/http.c +++ b/lib/http.c @@ -3889,136 +3889,112 @@ CURLcode Curl_bump_headersize(struct Curl_easy *data, return CURLE_OK; } -static CURLcode http_on_response(struct Curl_easy *data, - const char *last_hd, size_t last_hd_len, - const char *buf, size_t blen, - size_t *pconsumed) +/* + * Handle a 101 Switching Protocols response. Performs the actual protocol + * upgrade to HTTP/2 or WebSocket based on what was requested. + */ +static CURLcode http_on_101_upgrade(struct Curl_easy *data, + const char *buf, size_t blen, + size_t *pconsumed, + bool *conn_changed) { struct connectdata *conn = data->conn; - CURLcode result = CURLE_OK; struct SingleRequest *k = &data->req; - bool conn_changed = FALSE; - (void)buf; /* not used without HTTP2 enabled */ - *pconsumed = 0; - - if(k->upgr101 == UPGR101_RECEIVED) { - /* supposedly upgraded to http2 now */ - if(data->req.httpversion != 20) - infof(data, "Lying server, not serving HTTP/2"); - } - - if(k->httpcode < 200 && last_hd) { - /* Intermediate responses might trigger processing of more - * responses, write the last header to the client before - * proceeding. */ - result = http_write_header(data, last_hd, last_hd_len); - last_hd = NULL; /* handled it */ - if(result) - goto out; - } - - if(k->httpcode < 100) { - failf(data, "Unsupported response code in HTTP response"); - result = CURLE_UNSUPPORTED_PROTOCOL; - goto out; - } - else if(k->httpcode < 200) { - /* "A user agent MAY ignore unexpected 1xx status responses." - * By default, we expect to get more responses after this one. */ - k->header = TRUE; - k->headerline = 0; /* restart the header line counter */ - - switch(k->httpcode) { - case 100: - /* - * We have made an HTTP PUT or POST and this is 1.1-lingo - * that tells us that the server is OK with this and ready - * to receive the data. - */ - http_exp100_got100(data); - break; - case 101: { - int upgr101_requested = k->upgr101; - - if(k->httpversion_sent != 11) { - /* invalid for other HTTP versions */ - failf(data, "server sent 101 response while not talking HTTP/1.1"); - result = CURLE_WEIRD_SERVER_REPLY; - goto out; - } - - /* Whatever the success, upgrade was selected. */ - k->upgr101 = UPGR101_RECEIVED; - data->conn->bits.upgrade_in_progress = FALSE; - conn_changed = TRUE; - - /* To be fully conform, we would check the "Upgrade:" response header - * to mention the protocol we requested. */ - switch(upgr101_requested) { - case UPGR101_H2: - /* Switch to HTTP/2, where we will get more responses. - * blen bytes in bug are already h2 protocol bytes */ - infof(data, "Received 101, Switching to HTTP/2"); - result = Curl_http2_upgrade(data, conn, FIRSTSOCKET, buf, blen); - if(result) - goto out; - *pconsumed += blen; - break; -#ifndef CURL_DISABLE_WEBSOCKETS - case UPGR101_WS: - /* Switch to WebSocket, where we now stream ws frames. - * blen bytes in bug are already ws protocol bytes */ - infof(data, "Received 101, Switching to WebSocket"); - result = Curl_ws_accept(data, buf, blen); - if(result) - goto out; - *pconsumed += blen; /* ws accept handled the data */ - break; +#if !defined(USE_NGHTTP2) && defined(CURL_DISABLE_WEBSOCKETS) + (void)buf; + (void)blen; + (void)pconsumed; +#else + CURLcode result; + int upgr101_requested = k->upgr101; #endif - default: - /* We silently accept this as the final response. What are we - * switching to if we did not ask for an Upgrade? Maybe the - * application provided an `Upgrade: xxx` header? */ - k->header = FALSE; - break; - } - /* processed 101 */ - break; - } - default: - /* The server may send us other 1xx responses, like informative - * 103. This have no influence on request processing and we expect - * to receive a final response eventually. */ - break; - } - goto out; + + if(k->httpversion_sent != 11) { + /* invalid for other HTTP versions */ + failf(data, "server sent 101 response while not talking HTTP/1.1"); + return CURLE_WEIRD_SERVER_REPLY; } - /* k->httpcode >= 200, final response */ + /* Whatever the success, upgrade was selected. */ + k->upgr101 = UPGR101_RECEIVED; + conn->bits.upgrade_in_progress = FALSE; + *conn_changed = TRUE; + + /* To be fully compliant, we would check the "Upgrade:" response header to + * mention the protocol we requested. */ +#ifdef USE_NGHTTP2 + if(upgr101_requested == UPGR101_H2) { + /* Switch to HTTP/2, where we will get more responses. blen bytes in buf + * are already h2 protocol bytes */ + infof(data, "Received 101, Switching to HTTP/2"); + result = Curl_http2_upgrade(data, conn, FIRSTSOCKET, buf, blen); + if(!result) + *pconsumed += blen; + return result; + } +#endif +#ifndef CURL_DISABLE_WEBSOCKETS + if(upgr101_requested == UPGR101_WS) { + /* Switch to WebSocket, where we now stream ws frames. blen bytes in buf + * are already ws protocol bytes */ + infof(data, "Received 101, Switching to WebSocket"); + result = Curl_ws_accept(data, buf, blen); + if(!result) + *pconsumed += blen; /* ws accept handled the data */ + return result; + } +#endif + /* We silently accept this as the final response. What are we switching to + * if we did not ask for an Upgrade? Maybe the application provided an + * `Upgrade: xxx` header? */ k->header = FALSE; - if(data->conn->bits.upgrade_in_progress) { - /* Asked for protocol upgrade, but it was not selected by the server */ - data->conn->bits.upgrade_in_progress = FALSE; - conn_changed = TRUE; - } + return CURLE_OK; +} - if((k->size == -1) && !k->chunk && !conn->bits.close && - (k->httpversion == 11) && - !(conn->scheme->protocol & CURLPROTO_RTSP) && - data->state.httpreq != HTTPREQ_HEAD) { - /* On HTTP 1.1, when connection is not to get closed, but no - Content-Length nor Transfer-Encoding chunked have been - received, according to RFC2616 section 4.4 point 5, we - assume that the server will close the connection to - signal the end of the document. */ - infof(data, "no chunk, no close, no size. Assume close to " - "signal end"); - streamclose(conn, "HTTP: No end-of-message indicator"); - } +/* + * Handle 1xx intermediate HTTP responses. Sets up state for more + * headers and processes 100-continue and 101 upgrade responses. + */ +static CURLcode http_on_1xx_response(struct Curl_easy *data, + const char *buf, size_t blen, + size_t *pconsumed, + bool *conn_changed) +{ + struct SingleRequest *k = &data->req; - /* At this point we have some idea about the fate of the connection. - If we are closing the connection it may result auth failure. */ + /* "A user agent MAY ignore unexpected 1xx status responses." + * By default, we expect to get more responses after this one. */ + k->header = TRUE; + k->headerline = 0; /* restart the header line counter */ + + switch(k->httpcode) { + case 100: + /* We have made an HTTP PUT or POST and this is 1.1-lingo that tells us + * that the server is OK with this and ready to receive the data. */ + http_exp100_got100(data); + break; + case 101: + return http_on_101_upgrade(data, buf, blen, pconsumed, conn_changed); + default: + /* The server may send us other 1xx responses, like informative 103. This + * has no influence on request processing and we expect to receive a + * final response eventually. */ + break; + } + return CURLE_OK; +} + +#if defined(USE_NTLM) || defined(USE_SPNEGO) +/* + * Check if NTLM or SPNEGO authentication negotiation failed due to + * connection closure (typically on HTTP/1.0 servers). + */ +static void http_check_auth_closure(struct Curl_easy *data, + struct connectdata *conn) +{ + /* At this point we have some idea about the fate of the connection. If we + are closing the connection it may result auth failure. */ #ifdef USE_NTLM if(conn->bits.close && (((data->req.httpcode == 401) && @@ -4047,9 +4023,154 @@ static CURLcode http_on_response(struct Curl_easy *data, conn->proxy_negotiate_state = GSS_AUTHSUCC; } #endif +} +#else +#define http_check_auth_closure(x,y) /* empty */ +#endif + +/* + * Handle an error response (>= 300) received while still sending the + * request body. Deals with 417 Expectation Failed retries, keep-sending + * on error, and aborting the send. + */ +static CURLcode http_handle_send_error(struct Curl_easy *data) +{ + struct connectdata *conn = data->conn; + struct SingleRequest *k = &data->req; + CURLcode result = CURLE_OK; + + if(!data->req.authneg && !conn->bits.close && + !Curl_creader_will_rewind(data)) { + /* + * General treatment of errors when about to send data. + * Including: "417 Expectation Failed", while waiting for + * 100-continue. + * + * The check for close above is done because if something + * else has already deemed the connection to get closed then + * something else should have considered the big picture and + * we avoid this check. + */ + + switch(data->state.httpreq) { + case HTTPREQ_PUT: + case HTTPREQ_POST: + case HTTPREQ_POST_FORM: + case HTTPREQ_POST_MIME: + /* We got an error response. If this happened before the + * whole request body has been sent we stop sending and + * mark the connection for closure after we have read the + * entire response. */ + if(!Curl_req_done_sending(data)) { + if((k->httpcode == 417) && http_exp100_is_selected(data)) { + /* 417 Expectation Failed - try again without the + Expect header */ + if(!k->writebytecount && http_exp100_is_waiting(data)) { + infof(data, "Got HTTP failure 417 while waiting for a 100"); + } + else { + infof(data, "Got HTTP failure 417 while sending data"); + streamclose(conn, "Stop sending data before everything sent"); + result = http_perhapsrewind(data, conn); + if(result) + return result; + } + data->state.disableexpect = TRUE; + Curl_req_abort_sending(data); + DEBUGASSERT(!data->req.newurl); + data->req.newurl = Curl_bufref_dup(&data->state.url); + if(!data->req.newurl) + return CURLE_OUT_OF_MEMORY; + } + else if(data->set.http_keep_sending_on_error) { + infof(data, "HTTP error before end of send, keep sending"); + http_exp100_send_anyway(data); + } + else { + infof(data, "HTTP error before end of send, stop sending"); + streamclose(conn, "Stop sending data before everything sent"); + result = Curl_req_abort_sending(data); + if(result) + return result; + } + } + break; + + default: /* default label present to avoid compiler warnings */ + break; + } + } + + if(Curl_creader_will_rewind(data) && !Curl_req_done_sending(data)) { + /* We rewind before next send, continue sending now */ + infof(data, "Keep sending data to get tossed away"); + CURL_REQ_SET_SEND(data); + } + return result; +} + +static CURLcode http_on_response(struct Curl_easy *data, + const char *last_hd, size_t last_hd_len, + const char *buf, size_t blen, + size_t *pconsumed) +{ + struct connectdata *conn = data->conn; + CURLcode result = CURLE_OK; + struct SingleRequest *k = &data->req; + bool conn_changed = FALSE; + + (void)buf; /* not used without HTTP2 enabled */ + *pconsumed = 0; + + if(k->upgr101 == UPGR101_RECEIVED) { + /* supposedly upgraded to http2 now */ + if(data->req.httpversion != 20) + infof(data, "Lying server, not serving HTTP/2"); + } + + if(k->httpcode < 200 && last_hd) { + /* Intermediate responses might trigger processing of more responses, + * write the last header to the client before proceeding. */ + result = http_write_header(data, last_hd, last_hd_len); + last_hd = NULL; /* handled it */ + if(result) + goto out; + } + + if(k->httpcode < 100) { + failf(data, "Unsupported response code in HTTP response"); + result = CURLE_UNSUPPORTED_PROTOCOL; + goto out; + } + else if(k->httpcode < 200) { + result = http_on_1xx_response(data, buf, blen, pconsumed, &conn_changed); + goto out; + } + + /* k->httpcode >= 200, final response */ + k->header = FALSE; + if(conn->bits.upgrade_in_progress) { + /* Asked for protocol upgrade, but it was not selected */ + conn->bits.upgrade_in_progress = FALSE; + conn_changed = TRUE; + } + + if((k->size == -1) && !k->chunk && !conn->bits.close && + (k->httpversion == 11) && + !(conn->scheme->protocol & CURLPROTO_RTSP) && + data->state.httpreq != HTTPREQ_HEAD) { + /* On HTTP 1.1, when connection is not to get closed, but no + Content-Length nor Transfer-Encoding chunked have been received, + according to RFC2616 section 4.4 point 5, we assume that the server + will close the connection to signal the end of the document. */ + infof(data, "no chunk, no close, no size. Assume close to signal end"); + streamclose(conn, "HTTP: No end-of-message indicator"); + } + + http_check_auth_closure(data, conn); #ifndef CURL_DISABLE_WEBSOCKETS - /* All >=200 HTTP status codes are errors when wanting WebSocket */ + /* All >=200 HTTP status codes are errors when wanting ws */ if(data->req.upgr101 == UPGR101_WS) { failf(data, "Refused WebSocket upgrade: %d", k->httpcode); result = CURLE_HTTP_RETURNED_ERROR; @@ -4059,88 +4180,23 @@ static CURLcode http_on_response(struct Curl_easy *data, /* Check if this response means the transfer errored. */ if(http_should_fail(data, data->req.httpcode)) { - failf(data, "The requested URL returned error: %d", k->httpcode); + failf(data, "The requested URL returned error: %d", + k->httpcode); result = CURLE_HTTP_RETURNED_ERROR; goto out; } - /* Curl_http_auth_act() checks what authentication methods - * that are available and decides which one (if any) to - * use. It will set 'newurl' if an auth method was picked. */ + /* Curl_http_auth_act() checks what authentication methods that are + * available and decides which one (if any) to use. It will set 'newurl' if + * an auth method was picked. */ result = Curl_http_auth_act(data); if(result) goto out; if(k->httpcode >= 300) { - if(!data->req.authneg && !conn->bits.close && - !Curl_creader_will_rewind(data)) { - /* - * General treatment of errors when about to send data. Including : - * "417 Expectation Failed", while waiting for 100-continue. - * - * The check for close above is done because of something - * else has already deemed the connection to get closed then - * something else should have considered the big picture and we - * avoid this check. - * - */ - - switch(data->state.httpreq) { - case HTTPREQ_PUT: - case HTTPREQ_POST: - case HTTPREQ_POST_FORM: - case HTTPREQ_POST_MIME: - /* We got an error response. If this happened before the whole - * request body has been sent we stop sending and mark the - * connection for closure after we have read the entire response. - */ - if(!Curl_req_done_sending(data)) { - if((k->httpcode == 417) && http_exp100_is_selected(data)) { - /* 417 Expectation Failed - try again without the Expect - header */ - if(!k->writebytecount && http_exp100_is_waiting(data)) { - infof(data, "Got HTTP failure 417 while waiting for a 100"); - } - else { - infof(data, "Got HTTP failure 417 while sending data"); - streamclose(conn, "Stop sending data before everything sent"); - result = http_perhapsrewind(data, conn); - if(result) - goto out; - } - data->state.disableexpect = TRUE; - Curl_req_abort_sending(data); - DEBUGASSERT(!data->req.newurl); - data->req.newurl = Curl_bufref_dup(&data->state.url); - if(!data->req.newurl) { - result = CURLE_OUT_OF_MEMORY; - goto out; - } - } - else if(data->set.http_keep_sending_on_error) { - infof(data, "HTTP error before end of send, keep sending"); - http_exp100_send_anyway(data); - } - else { - infof(data, "HTTP error before end of send, stop sending"); - streamclose(conn, "Stop sending data before everything sent"); - result = Curl_req_abort_sending(data); - if(result) - goto out; - } - } - break; - - default: /* default label present to avoid compiler warnings */ - break; - } - } - - if(Curl_creader_will_rewind(data) && !Curl_req_done_sending(data)) { - /* We rewind before next send, continue sending now */ - infof(data, "Keep sending data to get tossed away"); - CURL_REQ_SET_SEND(data); - } + result = http_handle_send_error(data); + if(result) + goto out; } /* If we requested a "no body", this is a good time to get @@ -4149,11 +4205,10 @@ static CURLcode http_on_response(struct Curl_easy *data, if(data->req.no_body) k->download_done = TRUE; - /* If max download size is *zero* (nothing) we already have - nothing and can safely return ok now! For HTTP/2, we would - like to call http2_handle_stream_close to properly close a - stream. In order to do this, we keep reading until we - close the stream. */ + /* If max download size is *zero* (nothing) we already have nothing and can + safely return ok now! For HTTP/2, we would like to call + http2_handle_stream_close to properly close a stream. In order to do + this, we keep reading until we close the stream. */ if((k->maxdownload == 0) && (k->httpversion_sent < 20)) k->download_done = TRUE; @@ -4161,21 +4216,18 @@ static CURLcode http_on_response(struct Curl_easy *data, result = http_firstwrite(data); if(!result) - /* This is the last response that we get for the current request. - * Check on the body size and determine if the response is complete. - */ + /* This is the last response that we get for the current request. Check on + * the body size and determine if the response is complete. */ result = http_size(data); out: - if(last_hd) { + if(last_hd) /* if not written yet, write it now */ - result = Curl_1st_fatal( - result, http_write_header(data, last_hd, last_hd_len)); - } - if(conn_changed) { - /* poke the multi handle to allow any pending pipewait to retry now */ + result = Curl_1st_fatal(result, + http_write_header(data, last_hd, last_hd_len)); + if(conn_changed) + /* poke the multi handle to allow pending pipewait to retry */ Curl_multi_connchanged(data->multi); - } return result; } diff --git a/lib/http2.h b/lib/http2.h index a14c8485cc..e38dc5745c 100644 --- a/lib/http2.h +++ b/lib/http2.h @@ -70,7 +70,6 @@ extern struct Curl_cftype Curl_cft_nghttp2; #define Curl_http2_request_upgrade(x, y) CURLE_UNSUPPORTED_PROTOCOL #define Curl_http2_switch(a) CURLE_UNSUPPORTED_PROTOCOL -#define Curl_http2_upgrade(a, b, c, d, e) CURLE_UNSUPPORTED_PROTOCOL #define Curl_h2_http_1_1_error(x) 0 #endif