From patchwork Fri Apr 10 10:59:39 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Zahir Hussain X-Patchwork-Id: 85817 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id B363BF364DC for ; Fri, 10 Apr 2026 11:00:51 +0000 (UTC) Received: from mail-pg1-f175.google.com (mail-pg1-f175.google.com [209.85.215.175]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.153794.1775818843362628236 for ; Fri, 10 Apr 2026 04:00:43 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=IrwQf483; spf=pass (domain: gmail.com, ip: 209.85.215.175, mailfrom: mail2szahir@gmail.com) Received: by mail-pg1-f175.google.com with SMTP id 41be03b00d2f7-c76e702e01aso654884a12.3 for ; Fri, 10 Apr 2026 04:00:43 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1775818842; x=1776423642; darn=lists.openembedded.org; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:from:to:cc:subject:date:message-id:reply-to; bh=LzEsUbCQqlnts9lXzSxQXCSj24Y+HDDX/dvm+s3lcqo=; b=IrwQf48396eFe+jacFMkbJr8tf34Dyfmyt1mXaSLbrylbrGXorvw6OWjoRE5sF4j+X 3m3rb2fvaHQA+2nWxICzzUC5m1m54jjqaiYVLx0gYJpyQP9/TO3gc/TS1gTgM77oh70j TsIgT/OUo1sIl6GyqDVe7SZo1O3IQlnJla/w362yRvdoSNEzW3z3wscMVuaf/Ko0R5kt 9Z46g1mOcubZepSxh3oQcz11kWRYz56pIw4DXDgqIXmrBFh68Vg72Trx+vIEZTvZJS/Q +U+qTQeZoSWB7mPdZY40VMcKga5z2Zf2drPDTv3qRZhv8iFViziqkzNWAGCUZZOiVr8j lOuQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1775818842; x=1776423642; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=LzEsUbCQqlnts9lXzSxQXCSj24Y+HDDX/dvm+s3lcqo=; b=IqkyDcrZmLeCTT5i5164DG0g66MypyeO7K6CTZw+e0yv/GaUbGdcO9tWhXdupQgAjH 5h0e3bUndGk69ySSjinI08mWTWaroR2cBPq+ACM1Kmrn1L9z41MilqnhENo8c+40R9hY 7dZGFV0/K2LZWIehi17eOgm65nj0LQuKV9n1PB4v1M96t2K2PqfGgH/6EWAeIIPxNVFE YGtIhbIyP592XGmGuWrFvfn3hQn6cIAy+0Fkn9VAOetlkBcMAhQ7J+i71P/D0P41PGzn cWrQJ4bI5E8rU42MQSV64625oROjyM35pMaHoEoN46UDpcN6Wv6s56rSkqpXFCo2TOLD a5KQ== X-Gm-Message-State: AOJu0Yzi7OEi44rL7uMJxB2U/LIi4Eksy6lamFE/hkRI6NYn6eWc2DTg VgjUME/H4lPbIh1/DK/OLpQ95ak94Th1vC7qR3wrje29MwGsdnhmX/vlfr3spQ== X-Gm-Gg: AeBDiesdjekbouFDHtwKndhofKl62Is47WgvPym34wLmjv5ee7bhOv3mft3x1vt92GB NYI+70ZYlt49afh0t1YeAtb3Pn7xjF3zuj53qtlQ+2SRF/3IYaplUIFQCmniAM6cTildnN/KO+1 q2DIs7gZ9nIR0/yEvrFAxYkSIoq1+YuU5uUgtdMhvC8tWxhZbyepdXB/3XoKyZLoE1xt0oDSvS7 PTH0jP30tlQJEx+BoTNCkOC8Hj8Pil5IgUyeZnBsOxpGDPDeQ2NR0MAc5ic/EDKVSSeCDnNCiq6 CTPhmcGX5v2jW6XsMmNG+JNU8YTz4f20Sca2ie6gPKM08N88XqpwkI1uZR+TYmW+WZWj0A1NXqI XX1HxZiy+TeSjGRreHWl9wm07h9xt6gvPbaMu96hiKgENuYvF5CO4Kmo5RIUaRfqYCknM0phek9 qQu8T7s5UVippbelBbKF1aAhemLrIvHfZuhbcyUb63hQj/Zw== X-Received: by 2002:a05:6a20:7d9d:b0:39f:4d08:7555 with SMTP id adf61e73a8af0-39fe3c64bfemr3093881637.5.1775818841097; Fri, 10 Apr 2026 04:00:41 -0700 (PDT) Received: from L-17367L.kpit.com ([49.47.218.130]) by smtp.gmail.com with ESMTPSA id 41be03b00d2f7-c7921534918sm2275440a12.0.2026.04.10.04.00.38 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Fri, 10 Apr 2026 04:00:40 -0700 (PDT) From: Zahir Hussain X-Google-Original-From: Zahir Hussain To: openembedded-core@lists.openembedded.org, zahir.basha@kpit.com Cc: mail2szahir@gmail.com Subject: [OE-core][scarthgap][PATCH] curl: backport HTTP/2 and HTTP/3 error propagation fixes Date: Fri, 10 Apr 2026 16:29:39 +0530 Message-Id: <20260410105939.924439-1-zahir.basha@partner.bmwgroup.com> X-Mailer: git-send-email 2.34.1 MIME-Version: 1.0 List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Fri, 10 Apr 2026 11:00:51 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/235018 From: Zahir Hussain Backport upstream commits: https://github.com/curl/curl/commit/8dd81bd5db6d087c1d0f1b0332a5e8d0ad50d634 https://github.com/curl/curl/commit/5c59f91427c6e14036070d4e1426360393458b25 These fixes correct handling of transfer write errors in HTTP/2 and HTTP/3 (ngtcp2) during parallel transfers. In curl 8.7.1, write callback errors were not properly propagated, leading to incorrect exit codes and improper stream cancellation. Signed-off-by: Zahir Hussain --- ...-pass-CURLcode-errors-from-callbacks.patch | 333 ++++ .../lib-add-Curl_xfer_write_resp_hd.patch | 1746 +++++++++++++++++ meta/recipes-support/curl/curl_8.7.1.bb | 2 + 3 files changed, 2081 insertions(+) create mode 100644 meta/recipes-support/curl/curl/http2-ngtcp2-pass-CURLcode-errors-from-callbacks.patch create mode 100644 meta/recipes-support/curl/curl/lib-add-Curl_xfer_write_resp_hd.patch diff --git a/meta/recipes-support/curl/curl/http2-ngtcp2-pass-CURLcode-errors-from-callbacks.patch b/meta/recipes-support/curl/curl/http2-ngtcp2-pass-CURLcode-errors-from-callbacks.patch new file mode 100644 index 0000000000..814072d5af --- /dev/null +++ b/meta/recipes-support/curl/curl/http2-ngtcp2-pass-CURLcode-errors-from-callbacks.patch @@ -0,0 +1,333 @@ +From 5c59f91427c6e14036070d4e1426360393458b25 Mon Sep 17 00:00:00 2001 +From: Stefan Eissing +Date: Thu, 18 Apr 2024 23:24:34 +0200 +Subject: [PATCH] http2 + ngtcp2: pass CURLcode errors from callbacks + +- errors returned by Curl_xfer_write_resp() and the header variant are + not errors in the protocol. The result needs to be returned on the + next recv() from the protocol filter. + +- make xfer write errors for response data cause the stream to be + cancelled + +- added pytest test_02_14 and test_02_15 to verify that also for + parallel processing + +Reported-by: Laramie Leavitt +Fixes #13411 +Closes #13424 + +Upstream-Status: Backport [https://github.com/curl/curl/commit/5c59f91427c6e14036070d4e1426360393458b25] +Comment: Hunks are refreshed + +Signed-off-by: Zahir Hussain +--- + lib/http2.c | 64 +++++++++++++++++++++-------- + lib/vquic/curl_ngtcp2.c | 73 +++++++++++++++++++++++----------- + tests/http/test_02_download.py | 28 +++++++++++++ + 3 files changed, 125 insertions(+), 40 deletions(-) + +diff --git a/lib/http2.c b/lib/http2.c +index 1ee57a4320ac..50dd878bb02e 100644 +--- a/lib/http2.c ++++ b/lib/http2.c +@@ -193,6 +193,7 @@ struct h2_stream_ctx { + + int status_code; /* HTTP response status code */ + uint32_t error; /* stream error code */ ++ CURLcode xfer_result; /* Result of writing out response */ + uint32_t local_window_size; /* the local recv window size */ + int32_t id; /* HTTP/2 protocol identifier for stream */ + BIT(resp_hds_complete); /* we have a complete, final response */ +@@ -975,6 +976,41 @@ static int push_promise(struct Curl_cfilter *cf, + return rv; + } + ++static void h2_xfer_write_resp_hd(struct Curl_cfilter *cf, ++ struct Curl_easy *data, ++ struct h2_stream_ctx *stream, ++ const char *buf, size_t blen, bool eos) ++{ ++ ++ /* If we already encountered an error, skip further writes */ ++ if(!stream->xfer_result) { ++ stream->xfer_result = Curl_xfer_write_resp_hd(data, buf, blen, eos); ++ if(stream->xfer_result) ++ CURL_TRC_CF(data, cf, "[%d] error %d writing %zu bytes of headers", ++ stream->id, stream->xfer_result, blen); ++ } ++} ++ ++static void h2_xfer_write_resp(struct Curl_cfilter *cf, ++ struct Curl_easy *data, ++ struct h2_stream_ctx *stream, ++ const char *buf, size_t blen, bool eos) ++{ ++ ++ /* If we already encountered an error, skip further writes */ ++ if(!stream->xfer_result) ++ stream->xfer_result = Curl_xfer_write_resp(data, buf, blen, eos); ++ /* If the transfer write is errored, we do not want any more data */ ++ if(stream->xfer_result) { ++ struct cf_h2_ctx *ctx = cf->ctx; ++ CURL_TRC_CF(data, cf, "[%d] error %d writing %zu bytes of data, " ++ "RST-ing stream", ++ stream->id, stream->xfer_result, blen); ++ nghttp2_submit_rst_stream(ctx->h2, 0, stream->id, ++ NGHTTP2_ERR_CALLBACK_FAILURE); ++ } ++} ++ + static CURLcode on_stream_frame(struct Curl_cfilter *cf, + struct Curl_easy *data, + const nghttp2_frame *frame) +@@ -955,7 +955,6 @@ static CURLcode on_stream_frame(struct C + struct cf_h2_ctx *ctx = cf->ctx; + struct h2_stream_ctx *stream = H2_STREAM_CTX(data); + int32_t stream_id = frame->hd.stream_id; +- CURLcode result; + int rv; + + if(!stream) { +@@ -1030,9 +1065,7 @@ static CURLcode on_stream_frame(struct Curl_cfilter *cf, + stream->status_code = -1; + } + +- result = Curl_xfer_write_resp_hd(data, STRCONST("\r\n"), stream->closed); +- if(result) +- return result; ++ h2_xfer_write_resp_hd(cf, data, stream, STRCONST("\r\n"), stream->closed); + + if(stream->status_code / 100 != 1) { + stream->resp_hds_complete = TRUE; +@@ -1251,7 +1284,6 @@ static int on_data_chunk_recv(nghttp2_session *session, uint8_t flags, + struct cf_h2_ctx *ctx = cf->ctx; + struct h2_stream_ctx *stream; + struct Curl_easy *data_s; +- CURLcode result; + (void)flags; + + DEBUGASSERT(stream_id); /* should never be a zero stream ID here */ +@@ -1247,9 +1246,7 @@ static int on_data_chunk_recv(nghttp2_se + if(!stream) + return NGHTTP2_ERR_CALLBACK_FAILURE; + +- result = Curl_xfer_write_resp(data_s, (char *)mem, len, FALSE); +- if(result && result != CURLE_AGAIN) +- return NGHTTP2_ERR_CALLBACK_FAILURE; ++ h2_xfer_write_resp(cf, data_s, stream, (char *)mem, len, FALSE); + + nghttp2_session_consume(ctx->h2, stream_id, len); + stream->nrcvd_data += (curl_off_t)len; +@@ -1500,8 +1527,8 @@ static int on_header(nghttp2_session *session, const nghttp2_frame *frame, + if(!result) + result = Curl_dyn_addn(&ctx->scratch, STRCONST(" \r\n")); + if(!result) +- result = Curl_xfer_write_resp_hd(data_s, Curl_dyn_ptr(&ctx->scratch), +- Curl_dyn_len(&ctx->scratch), FALSE); ++ h2_xfer_write_resp_hd(cf, data_s, stream, Curl_dyn_ptr(&ctx->scratch), ++ Curl_dyn_len(&ctx->scratch), FALSE); + if(result) + return NGHTTP2_ERR_CALLBACK_FAILURE; + /* if we receive data for another handle, wake that up */ +@@ -1525,8 +1552,8 @@ static int on_header(nghttp2_session *session, const nghttp2_frame *frame, + if(!result) + result = Curl_dyn_addn(&ctx->scratch, STRCONST("\r\n")); + if(!result) +- result = Curl_xfer_write_resp_hd(data_s, Curl_dyn_ptr(&ctx->scratch), +- Curl_dyn_len(&ctx->scratch), FALSE); ++ h2_xfer_write_resp_hd(cf, data_s, stream, Curl_dyn_ptr(&ctx->scratch), ++ Curl_dyn_len(&ctx->scratch), FALSE); + if(result) + return NGHTTP2_ERR_CALLBACK_FAILURE; + /* if we receive data for another handle, wake that up */ +@@ -1831,7 +1858,12 @@ static ssize_t stream_recv(struct Curl_cfilter *cf, struct Curl_easy *data, + + (void)buf; + *err = CURLE_AGAIN; +- if(stream->closed) { ++ if(stream->xfer_result) { ++ CURL_TRC_CF(data, cf, "[%d] xfer write failed", stream->id); ++ *err = stream->xfer_result; ++ nread = -1; ++ } ++ else if(stream->closed) { + CURL_TRC_CF(data, cf, "[%d] returning CLOSE", stream->id); + nread = http2_handle_stream_close(cf, data, stream, err); + } +diff --git a/lib/vquic/curl_ngtcp2.c b/lib/vquic/curl_ngtcp2.c +index 50fe7af701ac..5171125f2fc4 100644 +--- a/lib/vquic/curl_ngtcp2.c ++++ b/lib/vquic/curl_ngtcp2.c +@@ -153,6 +153,7 @@ struct h3_stream_ctx { + uint64_t error3; /* HTTP/3 stream error code */ + curl_off_t upload_left; /* number of request bytes left to upload */ + int status_code; /* HTTP status code */ ++ CURLcode xfer_result; /* result from xfer_resp_write(_hd) */ + bool resp_hds_complete; /* we have a complete, final response */ + bool closed; /* TRUE on stream close */ + bool reset; /* TRUE on stream reset */ +@@ -795,6 +796,41 @@ static int cb_h3_stream_close(nghttp3_conn *conn, int64_t sid, + return 0; + } + ++static void h3_xfer_write_resp_hd(struct Curl_cfilter *cf, ++ struct Curl_easy *data, ++ struct h3_stream_ctx *stream, ++ const char *buf, size_t blen, bool eos) ++{ ++ ++ /* If we already encountered an error, skip further writes */ ++ if(!stream->xfer_result) { ++ stream->xfer_result = Curl_xfer_write_resp_hd(data, buf, blen, eos); ++ if(stream->xfer_result) ++ CURL_TRC_CF(data, cf, "[%"CURL_PRId64"] error %d writing %zu " ++ "bytes of headers", stream->id, stream->xfer_result, blen); ++ } ++} ++ ++static void h3_xfer_write_resp(struct Curl_cfilter *cf, ++ struct Curl_easy *data, ++ struct h3_stream_ctx *stream, ++ const char *buf, size_t blen, bool eos) ++{ ++ ++ /* If we already encountered an error, skip further writes */ ++ if(!stream->xfer_result) ++ stream->xfer_result = Curl_xfer_write_resp(data, buf, blen, eos); ++ /* If the transfer write is errored, we do not want any more data */ ++ if(stream->xfer_result) { ++ struct cf_ngtcp2_ctx *ctx = cf->ctx; ++ CURL_TRC_CF(data, cf, "[%"CURL_PRId64"] error %d writing %zu bytes " ++ "of data, cancelling stream", ++ stream->id, stream->xfer_result, blen); ++ nghttp3_conn_close_stream(ctx->h3conn, stream->id, ++ NGHTTP3_H3_REQUEST_CANCELLED); ++ } ++} ++ + static int cb_h3_recv_data(nghttp3_conn *conn, int64_t stream3_id, + const uint8_t *buf, size_t blen, + void *user_data, void *stream_user_data) +@@ -768,7 +769,6 @@ static int cb_h3_recv_data(nghttp3_conn + struct cf_ngtcp2_ctx *ctx = cf->ctx; + struct Curl_easy *data = stream_user_data; + struct h3_stream_ctx *stream = H3_STREAM_CTX(data); +- CURLcode result; + + (void)conn; + (void)stream3_id; +@@ -776,12 +776,7 @@ static int cb_h3_recv_data(nghttp3_conn + if(!stream) + return NGHTTP3_ERR_CALLBACK_FAILURE; + +- result = Curl_xfer_write_resp(data, (char *)buf, blen, FALSE); +- if(result) { +- CURL_TRC_CF(data, cf, "[%" PRId64 "] DATA len=%zu, ERROR receiving %d", +- stream->id, blen, result); +- return NGHTTP3_ERR_CALLBACK_FAILURE; +- } ++ h3_xfer_write_resp(cf, data, stream, (char *)buf, blen, FALSE); + if(blen) { + CURL_TRC_CF(data, cf, "[%" PRId64 "] ACK %zu bytes of DATA", + stream->id, blen); +@@ -814,7 +809,6 @@ static int cb_h3_end_headers(nghttp3_con + struct Curl_cfilter *cf = user_data; + struct Curl_easy *data = stream_user_data; + struct h3_stream_ctx *stream = H3_STREAM_CTX(data); +- CURLcode result = CURLE_OK; + (void)conn; + (void)stream_id; + (void)fin; +@@ -823,10 +817,7 @@ static int cb_h3_end_headers(nghttp3_con + if(!stream) + return 0; + /* add a CRLF only if we've received some headers */ +- result = Curl_xfer_write_resp_hd(data, STRCONST("\r\n"), stream->closed); +- if(result) { +- return -1; +- } ++ h3_xfer_write_resp_hd(cf, data, stream, STRCONST("\r\n"), stream->closed); + + CURL_TRC_CF(data, cf, "[%" PRId64 "] end_headers, status=%d", + stream_id, stream->status_code); +@@ -915,8 +937,8 @@ static int cb_h3_recv_header(nghttp3_conn *conn, int64_t sid, + if(!result) + result = Curl_dyn_addn(&ctx->scratch, STRCONST(" \r\n")); + if(!result) +- result = Curl_xfer_write_resp_hd(data, Curl_dyn_ptr(&ctx->scratch), +- Curl_dyn_len(&ctx->scratch), FALSE); ++ h3_xfer_write_resp_hd(cf, data, stream, Curl_dyn_ptr(&ctx->scratch), ++ Curl_dyn_len(&ctx->scratch), FALSE); + CURL_TRC_CF(data, cf, "[%" CURL_PRId64 "] status: %s", + stream_id, Curl_dyn_ptr(&ctx->scratch)); + if(result) { +@@ -939,11 +961,8 @@ static int cb_h3_recv_header(nghttp3_conn *conn, int64_t sid, + if(!result) + result = Curl_dyn_addn(&ctx->scratch, STRCONST("\r\n")); + if(!result) +- result = Curl_xfer_write_resp_hd(data, Curl_dyn_ptr(&ctx->scratch), +- Curl_dyn_len(&ctx->scratch), FALSE); +- if(result) { +- return -1; +- } ++ h3_xfer_write_resp_hd(cf, data, stream, Curl_dyn_ptr(&ctx->scratch), ++ Curl_dyn_len(&ctx->scratch), FALSE); + } + return 0; + } +@@ -1128,7 +1147,13 @@ static ssize_t cf_ngtcp2_recv(struct Curl_cfilter *cf, struct Curl_easy *data, + goto out; + } + +- if(stream->closed) { ++ if(stream->xfer_result) { ++ CURL_TRC_CF(data, cf, "[%" CURL_PRId64 "] xfer write failed", stream->id); ++ *err = stream->xfer_result; ++ nread = -1; ++ goto out; ++ } ++ else if(stream->closed) { + nread = recv_closed_stream(cf, data, stream, err); + goto out; + } +diff --git a/tests/http/test_02_download.py b/tests/http/test_02_download.py +index 95a30e114b27..74f2e1e20fec 100644 +--- a/tests/http/test_02_download.py ++++ b/tests/http/test_02_download.py +@@ -257,6 +257,34 @@ def test_02_13_head_serial_h2c(self, env: Env, + ]) + r.check_response(count=count, http_status=200) + ++ @pytest.mark.parametrize("proto", ['h2', 'h3']) ++ def test_02_14_not_found(self, env: Env, httpd, nghttpx, repeat, proto): ++ if proto == 'h3' and not env.have_h3(): ++ pytest.skip("h3 not supported") ++ if proto == 'h3' and env.curl_uses_lib('msh3'): ++ pytest.skip("msh3 stalls here") ++ count = 10 ++ urln = f'https://{env.authority_for(env.domain1, proto)}/not-found?[0-{count-1}]' ++ curl = CurlClient(env=env) ++ r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[ ++ '--parallel' ++ ]) ++ r.check_stats(count=count, http_status=404, exitcode=0) ++ ++ @pytest.mark.parametrize("proto", ['h2', 'h3']) ++ def test_02_15_fail_not_found(self, env: Env, httpd, nghttpx, repeat, proto): ++ if proto == 'h3' and not env.have_h3(): ++ pytest.skip("h3 not supported") ++ if proto == 'h3' and env.curl_uses_lib('msh3'): ++ pytest.skip("msh3 stalls here") ++ count = 10 ++ urln = f'https://{env.authority_for(env.domain1, proto)}/not-found?[0-{count-1}]' ++ curl = CurlClient(env=env) ++ r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[ ++ '--fail' ++ ]) ++ r.check_stats(count=count, http_status=404, exitcode=22) ++ + @pytest.mark.skipif(condition=Env().slow_network, reason="not suitable for slow network tests") + @pytest.mark.skipif(condition=Env().ci_run, reason="not suitable for CI runs") + def test_02_20_h2_small_frames(self, env: Env, httpd, repeat): + diff --git a/meta/recipes-support/curl/curl/lib-add-Curl_xfer_write_resp_hd.patch b/meta/recipes-support/curl/curl/lib-add-Curl_xfer_write_resp_hd.patch new file mode 100644 index 0000000000..1a9d5dab46 --- /dev/null +++ b/meta/recipes-support/curl/curl/lib-add-Curl_xfer_write_resp_hd.patch @@ -0,0 +1,1746 @@ +From 8dd81bd5db6d087c1d0f1b0332a5e8d0ad50d634 Mon Sep 17 00:00:00 2001 +From: Stefan Eissing +Date: Thu, 21 Mar 2024 12:15:59 +0100 +Subject: [PATCH] lib: add Curl_xfer_write_resp_hd + +Add method in protocol handlers to allow writing of a single, +0-terminated header line. Avoids parsing and copying these lines. + +Closes #13165 + +Upstream-Status: Backport [https://github.com/curl/curl/commit/8dd81bd5db6d087c1d0f1b0332a5e8d0ad50d634] +Comment: Few hunks are refreshed + +Signed-off-by: Zahir Hussain +--- + lib/c-hyper.c | 2 +- + lib/curl_rtmp.c | 6 + + lib/dict.c | 1 + + lib/file.c | 1 + + lib/ftp.c | 2 + + lib/gopher.c | 2 + + lib/http.c | 661 ++++++++++++++++++++-------------------- + lib/http.h | 7 +- + lib/http2.c | 52 ++-- + lib/imap.c | 2 + + lib/ldap.c | 2 + + lib/mqtt.c | 1 + + lib/openldap.c | 2 + + lib/pop3.c | 4 +- + lib/pop3.h | 3 +- + lib/request.c | 2 +- + lib/rtsp.c | 17 +- + lib/rtsp.h | 2 +- + lib/smb.c | 2 + + lib/smtp.c | 2 + + lib/telnet.c | 1 + + lib/tftp.c | 1 + + lib/transfer.c | 14 +- + lib/transfer.h | 12 +- + lib/urldata.h | 8 +- + lib/vquic/curl_ngtcp2.c | 56 ++-- + lib/vssh/libssh.c | 2 + + lib/vssh/libssh2.c | 2 + + lib/vssh/wolfssh.c | 2 + + lib/ws.c | 2 + + 30 files changed, 471 insertions(+), 402 deletions(-) + +diff --git a/lib/c-hyper.c b/lib/c-hyper.c +index 88674ee0a1ef..0593d9706586 100644 +--- a/lib/c-hyper.c ++++ b/lib/c-hyper.c +@@ -171,7 +171,7 @@ static int hyper_each_header(void *userdata, + len = Curl_dyn_len(&data->state.headerb); + headp = Curl_dyn_ptr(&data->state.headerb); + +- result = Curl_http_header(data, data->conn, headp, len); ++ result = Curl_http_header(data, headp, len); + if(result) { + data->state.hresult = result; + return HYPER_ITER_BREAK; +diff --git a/lib/curl_rtmp.c b/lib/curl_rtmp.c +index b2f2adad8b19..c1fd981b7869 100644 +--- a/lib/curl_rtmp.c ++++ b/lib/curl_rtmp.c +@@ -80,6 +80,7 @@ const struct Curl_handler Curl_handler_rtmp = { + ZERO_NULL, /* perform_getsock */ + rtmp_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_RTMP, /* defport */ +@@ -103,6 +104,7 @@ const struct Curl_handler Curl_handler_rtmpt = { + ZERO_NULL, /* perform_getsock */ + rtmp_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_RTMPT, /* defport */ +@@ -126,6 +128,7 @@ const struct Curl_handler Curl_handler_rtmpe = { + ZERO_NULL, /* perform_getsock */ + rtmp_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_RTMP, /* defport */ +@@ -149,6 +152,7 @@ const struct Curl_handler Curl_handler_rtmpte = { + ZERO_NULL, /* perform_getsock */ + rtmp_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_RTMPT, /* defport */ +@@ -172,6 +176,7 @@ const struct Curl_handler Curl_handler_rtmps = { + ZERO_NULL, /* perform_getsock */ + rtmp_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_RTMPS, /* defport */ +@@ -195,6 +200,7 @@ const struct Curl_handler Curl_handler_rtmpts = { + ZERO_NULL, /* perform_getsock */ + rtmp_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_RTMPS, /* defport */ +diff --git a/lib/dict.c b/lib/dict.c +index f37767882e7c..5404671c646d 100644 +--- a/lib/dict.c ++++ b/lib/dict.c +@@ -90,6 +90,7 @@ const struct Curl_handler Curl_handler_dict = { + ZERO_NULL, /* perform_getsock */ + ZERO_NULL, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_DICT, /* defport */ +diff --git a/lib/file.c b/lib/file.c +index bee9e92ecaa5..c436aaaad47d 100644 +--- a/lib/file.c ++++ b/lib/file.c +@@ -115,6 +115,7 @@ const struct Curl_handler Curl_handler_file = { + ZERO_NULL, /* perform_getsock */ + file_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + 0, /* defport */ +diff --git a/lib/ftp.c b/lib/ftp.c +index 5bbdeb054040..760b54ff3bc6 100644 +--- a/lib/ftp.c ++++ b/lib/ftp.c +@@ -177,6 +177,7 @@ const struct Curl_handler Curl_handler_ftp = { + ZERO_NULL, /* perform_getsock */ + ftp_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_FTP, /* defport */ +@@ -208,6 +209,7 @@ const struct Curl_handler Curl_handler_ftps = { + ZERO_NULL, /* perform_getsock */ + ftp_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_FTPS, /* defport */ +diff --git a/lib/gopher.c b/lib/gopher.c +index e1a1ba648862..7ba070a769b3 100644 +--- a/lib/gopher.c ++++ b/lib/gopher.c +@@ -76,6 +76,7 @@ const struct Curl_handler Curl_handler_gopher = { + ZERO_NULL, /* perform_getsock */ + ZERO_NULL, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_GOPHER, /* defport */ +@@ -100,6 +101,7 @@ const struct Curl_handler Curl_handler_gophers = { + ZERO_NULL, /* perform_getsock */ + ZERO_NULL, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_GOPHER, /* defport */ +diff --git a/lib/http.c b/lib/http.c +index 7cb8b63f8c17..dea166c3dddd 100644 +--- a/lib/http.c ++++ b/lib/http.c +@@ -100,7 +100,7 @@ + * Forward declarations. + */ + +-static bool http_should_fail(struct Curl_easy *data); ++static bool http_should_fail(struct Curl_easy *data, int httpcode); + 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); +@@ -123,6 +123,7 @@ const struct Curl_handler Curl_handler_http = { + ZERO_NULL, /* perform_getsock */ + ZERO_NULL, /* disconnect */ + Curl_http_write_resp, /* write_resp */ ++ Curl_http_write_resp_hd, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_HTTP, /* defport */ +@@ -151,6 +152,7 @@ const struct Curl_handler Curl_handler_https = { + ZERO_NULL, /* perform_getsock */ + ZERO_NULL, /* disconnect */ + Curl_http_write_resp, /* write_resp */ ++ Curl_http_write_resp_hd, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_HTTPS, /* defport */ +@@ -243,8 +245,6 @@ char *Curl_copy_header_value(const char *header) + while(*start && ISSPACE(*start)) + start++; + +- /* data is in the host encoding so +- use '\r' and '\n' instead of 0x0d and 0x0a */ + end = strchr(start, '\r'); + if(!end) + end = strchr(start, '\n'); +@@ -565,7 +565,7 @@ CURLcode Curl_http_auth_act(struct Curl_easy *data) + data->state.authhost.done = TRUE; + } + } +- if(http_should_fail(data)) { ++ if(http_should_fail(data, data->req.httpcode)) { + failf(data, "The requested URL returned error: %d", + data->req.httpcode); + result = CURLE_HTTP_RETURNED_ERROR; +@@ -1006,21 +1006,18 @@ CURLcode Curl_http_input_auth(struct Curl_easy *data, bool proxy, + } + + /** +- * http_should_fail() determines whether an HTTP response has gotten us ++ * http_should_fail() determines whether an HTTP response code has gotten us + * into an error state or not. + * + * @retval FALSE communications should continue + * + * @retval TRUE communications should not continue + */ +-static bool http_should_fail(struct Curl_easy *data) ++static bool http_should_fail(struct Curl_easy *data, int httpcode) + { +- int httpcode; + DEBUGASSERT(data); + DEBUGASSERT(data->conn); + +- httpcode = data->req.httpcode; +- + /* + ** If we haven't been asked to fail on error, + ** don't fail. +@@ -2836,9 +2833,10 @@ checkprotoprefix(struct Curl_easy *data, struct connectdata *conn, + /* + * Curl_http_header() parses a single response header. + */ +-CURLcode Curl_http_header(struct Curl_easy *data, struct connectdata *conn, +- char *hd, size_t hdlen) ++CURLcode Curl_http_header(struct Curl_easy *data, ++ const char *hd, size_t hdlen) + { ++ struct connectdata *conn = data->conn; + CURLcode result; + struct SingleRequest *k = &data->req; + const char *v; +@@ -2848,7 +2846,7 @@ CURLcode Curl_http_header(struct Curl_easy *data, struct connectdata *conn, + case 'A': + #ifndef CURL_DISABLE_ALTSVC + v = (data->asi && +- ((conn->handler->flags & PROTOPT_SSL) || ++ ((data->conn->handler->flags & PROTOPT_SSL) || + #ifdef CURLDEBUG + /* allow debug builds to circumvent the HTTPS restriction */ + getenv("CURL_ALTSVC_HTTP") +@@ -3306,12 +3304,11 @@ CURLcode Curl_http_size(struct Curl_easy *data) + return CURLE_OK; + } + +-static CURLcode verify_header(struct Curl_easy *data) ++static CURLcode verify_header(struct Curl_easy *data, ++ const char *hd, size_t hdlen) + { + struct SingleRequest *k = &data->req; +- const char *header = Curl_dyn_ptr(&data->state.headerb); +- size_t hlen = Curl_dyn_len(&data->state.headerb); +- char *ptr = memchr(header, 0x00, hlen); ++ char *ptr = memchr(hd, 0x00, hdlen); + if(ptr) { + /* this is bad, bail out */ + failf(data, "Nul byte in header"); +@@ -3320,11 +3317,11 @@ static CURLcode verify_header(struct Curl_easy *data) + if(k->headerline < 2) + /* the first "header" is the status-line and it has no colon */ + return CURLE_OK; +- if(((header[0] == ' ') || (header[0] == '\t')) && k->headerline > 2) ++ if(((hd[0] == ' ') || (hd[0] == '\t')) && k->headerline > 2) + /* line folding, can't happen on line 2 */ + ; + else { +- ptr = memchr(header, ':', hlen); ++ ptr = memchr(hd, ':', hdlen); + if(!ptr) { + /* this is bad, bail out */ + failf(data, "Header without colon"); +@@ -3369,7 +3366,6 @@ static CURLcode http_on_response(struct Curl_easy *data, + struct connectdata *conn = data->conn; + CURLcode result = CURLE_OK; + struct SingleRequest *k = &data->req; +- bool switch_to_h2 = FALSE; + + (void)buf; /* not used without HTTP2 enabled */ + *pconsumed = 0; +@@ -3388,96 +3384,92 @@ static CURLcode http_on_response(struct Curl_easy *data, + return CURLE_UNSUPPORTED_PROTOCOL; + } + else if(k->httpcode < 200) { +- /* "A user agent MAY ignore unexpected 1xx status responses." */ ++ /* "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. +- * However, we'll get more headers now so we must get +- * back into the header-parsing state! + */ +- k->header = TRUE; +- k->headerline = 0; /* restart the header line counter */ +- +- /* if we did wait for this do enable write now! */ + Curl_http_exp100_got100(data); + break; + case 101: +- if(conn->httpversion == 11) { +- /* Switching Protocols only allowed from HTTP/1.1 */ +- if(k->upgr101 == UPGR101_H2) { +- /* Switching to HTTP/2 */ +- infof(data, "Received 101, Switching to HTTP/2"); +- k->upgr101 = UPGR101_RECEIVED; +- +- /* we'll get more headers (HTTP/2 response) */ +- k->header = TRUE; +- k->headerline = 0; /* restart the header line counter */ +- switch_to_h2 = TRUE; +- } +-#ifdef USE_WEBSOCKETS +- else if(k->upgr101 == UPGR101_WS) { +- /* verify the response */ +- result = Curl_ws_accept(data, buf, blen); +- if(result) +- return result; +- k->header = FALSE; /* no more header to parse! */ +- *pconsumed += blen; /* ws accept handled the data */ +- blen = 0; +- if(data->set.connect_only) +- k->keepon &= ~KEEP_RECV; /* read no more content */ +- } +-#endif +- else { +- /* Not switching to another protocol */ +- k->header = FALSE; /* no more header to parse! */ +- } +- } +- else { ++ /* Switching Protocols only allowed from HTTP/1.1 */ ++ if(conn->httpversion != 11) { + /* invalid for other HTTP versions */ + failf(data, "unexpected 101 response code"); + return CURLE_WEIRD_SERVER_REPLY; + } ++ if(k->upgr101 == UPGR101_H2) { ++ /* Switching to HTTP/2, where we will get more responses */ ++ infof(data, "Received 101, Switching to HTTP/2"); ++ k->upgr101 = UPGR101_RECEIVED; ++ /* We expect more response from HTTP/2 later */ ++ k->header = TRUE; ++ k->headerline = 0; /* restart the header line counter */ ++ /* Any remaining `buf` bytes are already HTTP/2 and passed to ++ * be processed. */ ++ result = Curl_http2_upgrade(data, conn, FIRSTSOCKET, buf, blen); ++ if(result) ++ return result; ++ *pconsumed += blen; ++ } ++#ifdef USE_WEBSOCKETS ++ else if(k->upgr101 == UPGR101_WS) { ++ /* verify the response. Any passed `buf` bytes are already in ++ * WebSockets format and taken in by the protocol handler. */ ++ result = Curl_ws_accept(data, buf, blen); ++ if(result) ++ return result; ++ *pconsumed += blen; /* ws accept handled the data */ ++ k->header = FALSE; /* we will not get more responses */ ++ if(data->set.connect_only) ++ k->keepon &= ~KEEP_RECV; /* read no more content */ ++ } ++#endif ++ else { ++ /* We silently accept this as the final response. ++ * TODO: this looks, uhm, wrong. 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; + default: +- /* the status code 1xx indicates a provisional response, so +- we'll get another set of headers */ +- k->header = TRUE; +- k->headerline = 0; /* restart the header line counter */ ++ /* 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; + } ++ return result; + } +- else { +- /* k->httpcode >= 200, final response */ +- k->header = FALSE; + +- if(k->upgr101 == UPGR101_H2) { +- /* A requested upgrade was denied, poke the multi handle to possibly +- allow a pending pipewait to continue */ +- Curl_multi_connchanged(data->multi); +- } ++ /* k->httpcode >= 200, final response */ ++ k->header = FALSE; + +- if((k->size == -1) && !k->chunk && !conn->bits.close && +- (conn->httpversion == 11) && +- !(conn->handler->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"); +- } ++ if(k->upgr101 == UPGR101_H2) { ++ /* A requested upgrade was denied, poke the multi handle to possibly ++ allow a pending pipewait to continue */ ++ Curl_multi_connchanged(data->multi); + } + +- if(!k->header) { +- result = Curl_http_size(data); +- if(result) +- return result; ++ if((k->size == -1) && !k->chunk && !conn->bits.close && ++ (conn->httpversion == 11) && ++ !(conn->handler->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"); + } + + /* At this point we have some idea about the fate of the connection. +@@ -3511,31 +3503,25 @@ static CURLcode http_on_response(struct Curl_easy *data, + } + #endif + +- /* +- * When all the headers have been parsed, see if we should give +- * up and return an error. +- */ +- if(http_should_fail(data)) { +- failf(data, "The requested URL returned error: %d", +- k->httpcode); +- return CURLE_HTTP_RETURNED_ERROR; +- } +- + #ifdef USE_WEBSOCKETS +- /* All non-101 HTTP status codes are bad when wanting to upgrade to +- websockets */ ++ /* All >=200 HTTP status codes are errors when wanting websockets */ + if(data->req.upgr101 == UPGR101_WS) { + failf(data, "Refused WebSockets upgrade: %d", k->httpcode); + return CURLE_HTTP_RETURNED_ERROR; + } + #endif + ++ /* 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); ++ return CURLE_HTTP_RETURNED_ERROR; ++ } + + /* 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) + return result; + +@@ -3606,65 +3592,244 @@ static CURLcode http_on_response(struct Curl_easy *data, + infof(data, "Keep sending data to get tossed away"); + k->keepon |= KEEP_SEND; + } ++ + } + +- if(!k->header) { +- /* +- * really end-of-headers. +- * +- * If we requested a "no body", this is a good time to get +- * out and return home. ++ /* This is the last response that we will got for the current request. ++ * Check on the body size and determine if the response is complete. ++ */ ++ result = Curl_http_size(data); ++ if(result) ++ return result; ++ ++ /* If we requested a "no body", this is a good time to get ++ * out and return home. ++ */ ++ 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! But for HTTP/2, we'd ++ 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(0 == k->maxdownload ++ && !Curl_conn_is_http2(data, conn, FIRSTSOCKET) ++ && !Curl_conn_is_http3(data, conn, FIRSTSOCKET)) ++ k->download_done = TRUE; ++ ++ /* final response without error, prepare to receive the body */ ++ return Curl_http_firstwrite(data); ++} ++ ++static CURLcode http_rw_hd(struct Curl_easy *data, ++ const char *hd, size_t hdlen, ++ const char *buf_remain, size_t blen, ++ size_t *pconsumed) ++{ ++ CURLcode result = CURLE_OK; ++ struct SingleRequest *k = &data->req; ++ int writetype; ++ ++ *pconsumed = 0; ++ if((0x0a == *hd) || (0x0d == *hd)) { ++ /* Empty header line means end of headers! */ ++ size_t consumed; ++ ++ /* now, only output this if the header AND body are requested: + */ +- 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! But for HTTP/2, we'd +- 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(0 == k->maxdownload +- && !Curl_conn_is_http2(data, conn, FIRSTSOCKET) +- && !Curl_conn_is_http3(data, conn, FIRSTSOCKET)) +- k->download_done = TRUE; +- } +- +- if(switch_to_h2) { +- /* Having handled the headers, we can do the HTTP/2 switch. +- * Any remaining `buf` bytes are already HTTP/2 and passed to +- * be processed. */ +- result = Curl_http2_upgrade(data, conn, FIRSTSOCKET, buf, blen); ++ Curl_debug(data, CURLINFO_HEADER_IN, (char *)hd, hdlen); ++ ++ writetype = CLIENTWRITE_HEADER | ++ ((k->httpcode/100 == 1) ? CLIENTWRITE_1XX : 0); ++ ++ result = Curl_client_write(data, writetype, hd, hdlen); ++ if(result) ++ return result; ++ ++ result = Curl_bump_headersize(data, hdlen, FALSE); ++ if(result) ++ return result; ++ ++ data->req.deductheadercount = ++ (100 <= k->httpcode && 199 >= k->httpcode)?data->req.headerbytecount:0; ++ ++ /* analyze the response to find out what to do. */ ++ /* Caveat: we clear anything in the header brigade, because a ++ * response might switch HTTP version which may call use recursively. ++ * Not nice, but that is currently the way of things. */ ++ Curl_dyn_reset(&data->state.headerb); ++ result = http_on_response(data, buf_remain, blen, &consumed); + if(result) + return result; +- *pconsumed += blen; ++ *pconsumed += consumed; ++ return CURLE_OK; + } + ++ /* ++ * Checks for special headers coming up. ++ */ ++ ++ writetype = CLIENTWRITE_HEADER; ++ if(!k->headerline++) { ++ /* This is the first header, it MUST be the error code line ++ or else we consider this to be the body right away! */ ++ bool fine_statusline = FALSE; ++ ++ k->httpversion = 0; /* Don't know yet */ ++ if(data->conn->handler->protocol & PROTO_FAMILY_HTTP) { ++ /* ++ * https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2 ++ * ++ * The response code is always a three-digit number in HTTP as the spec ++ * says. We allow any three-digit number here, but we cannot make ++ * guarantees on future behaviors since it isn't within the protocol. ++ */ ++ const char *p = hd; ++ ++ while(*p && ISBLANK(*p)) ++ p++; ++ if(!strncmp(p, "HTTP/", 5)) { ++ p += 5; ++ switch(*p) { ++ case '1': ++ p++; ++ if((p[0] == '.') && (p[1] == '0' || p[1] == '1')) { ++ if(ISBLANK(p[2])) { ++ k->httpversion = 10 + (p[1] - '0'); ++ p += 3; ++ if(ISDIGIT(p[0]) && ISDIGIT(p[1]) && ISDIGIT(p[2])) { ++ k->httpcode = (p[0] - '0') * 100 + (p[1] - '0') * 10 + ++ (p[2] - '0'); ++ p += 3; ++ if(ISSPACE(*p)) ++ fine_statusline = TRUE; ++ } ++ } ++ } ++ if(!fine_statusline) { ++ failf(data, "Unsupported HTTP/1 subversion in response"); ++ return CURLE_UNSUPPORTED_PROTOCOL; ++ } ++ break; ++ case '2': ++ case '3': ++ if(!ISBLANK(p[1])) ++ break; ++ k->httpversion = (*p - '0') * 10; ++ p += 2; ++ if(ISDIGIT(p[0]) && ISDIGIT(p[1]) && ISDIGIT(p[2])) { ++ k->httpcode = (p[0] - '0') * 100 + (p[1] - '0') * 10 + ++ (p[2] - '0'); ++ p += 3; ++ if(!ISSPACE(*p)) ++ break; ++ fine_statusline = TRUE; ++ } ++ break; ++ default: /* unsupported */ ++ failf(data, "Unsupported HTTP version in response"); ++ return CURLE_UNSUPPORTED_PROTOCOL; ++ } ++ } ++ ++ if(!fine_statusline) { ++ /* If user has set option HTTP200ALIASES, ++ compare header line against list of aliases ++ */ ++ statusline check = checkhttpprefix(data, hd, hdlen); ++ if(check == STATUS_DONE) { ++ fine_statusline = TRUE; ++ k->httpcode = 200; ++ k->httpversion = 10; ++ } ++ } ++ } ++ else if(data->conn->handler->protocol & CURLPROTO_RTSP) { ++ const char *p = hd; ++ while(*p && ISBLANK(*p)) ++ p++; ++ if(!strncmp(p, "RTSP/", 5)) { ++ p += 5; ++ if(ISDIGIT(*p)) { ++ p++; ++ if((p[0] == '.') && ISDIGIT(p[1])) { ++ if(ISBLANK(p[2])) { ++ p += 3; ++ if(ISDIGIT(p[0]) && ISDIGIT(p[1]) && ISDIGIT(p[2])) { ++ k->httpcode = (p[0] - '0') * 100 + (p[1] - '0') * 10 + ++ (p[2] - '0'); ++ p += 3; ++ if(ISSPACE(*p)) { ++ fine_statusline = TRUE; ++ k->httpversion = 11; /* RTSP acts like HTTP 1.1 */ ++ } ++ } ++ } ++ } ++ } ++ if(!fine_statusline) ++ return CURLE_WEIRD_SERVER_REPLY; ++ } ++ } ++ ++ if(fine_statusline) { ++ result = Curl_http_statusline(data, data->conn); ++ if(result) ++ return result; ++ writetype |= CLIENTWRITE_STATUS; ++ } ++ else { ++ k->header = FALSE; /* this is not a header line */ ++ return CURLE_WEIRD_SERVER_REPLY; ++ } ++ } ++ ++ result = verify_header(data, hd, hdlen); ++ if(result) ++ return result; ++ ++ result = Curl_http_header(data, hd, hdlen); ++ if(result) ++ return result; ++ ++ /* ++ * Taken in one (more) header. Write it to the client. ++ */ ++ Curl_debug(data, CURLINFO_HEADER_IN, (char *)hd, hdlen); ++ ++ if(k->httpcode/100 == 1) ++ writetype |= CLIENTWRITE_1XX; ++ result = Curl_client_write(data, writetype, hd, hdlen); ++ if(result) ++ return result; ++ ++ result = Curl_bump_headersize(data, hdlen, FALSE); ++ if(result) ++ return result; ++ + return CURLE_OK; + } ++ + /* + * Read any HTTP header lines from the server and pass them to the client app. + */ +-static CURLcode http_rw_headers(struct Curl_easy *data, +- const char *buf, size_t blen, +- size_t *pconsumed) ++static CURLcode http_parse_headers(struct Curl_easy *data, ++ const char *buf, size_t blen, ++ size_t *pconsumed) + { + struct connectdata *conn = data->conn; + CURLcode result = CURLE_OK; + struct SingleRequest *k = &data->req; +- char *hd; +- size_t hdlen; + char *end_ptr; + bool leftover_body = FALSE; + + /* header line within buffer loop */ + *pconsumed = 0; +- do { +- size_t line_length; +- int writetype; +- +- /* data is in network encoding so use 0x0a instead of '\n' */ +- end_ptr = memchr(buf, 0x0a, blen); ++ while(blen && k->header) { ++ size_t consumed; + ++ end_ptr = memchr(buf, '\n', blen); + if(!end_ptr) { + /* Not a complete header line within buffer, append the data to + the end of the headerbuff. */ +@@ -3700,14 +3865,13 @@ static CURLcode http_rw_headers(struct Curl_easy *data, + } + + /* decrease the size of the remaining (supposed) header line */ +- line_length = (end_ptr - buf) + 1; +- result = Curl_dyn_addn(&data->state.headerb, buf, line_length); ++ consumed = (end_ptr - buf) + 1; ++ result = Curl_dyn_addn(&data->state.headerb, buf, consumed); + if(result) + return result; +- +- blen -= line_length; +- buf += line_length; +- *pconsumed += line_length; ++ blen -= consumed; ++ buf += consumed; ++ *pconsumed += consumed; + + /**** + * We now have a FULL header line in 'headerb'. +@@ -3735,195 +3899,21 @@ static CURLcode http_rw_headers(struct Curl_easy *data, + } + } + +- /* headers are in network encoding so use 0x0a and 0x0d instead of '\n' +- and '\r' */ +- hd = Curl_dyn_ptr(&data->state.headerb); +- hdlen = Curl_dyn_len(&data->state.headerb); +- if((0x0a == *hd) || (0x0d == *hd)) { +- /* Empty header line means end of headers! */ +- size_t consumed; +- +- /* now, only output this if the header AND body are requested: +- */ +- Curl_debug(data, CURLINFO_HEADER_IN, hd, hdlen); +- +- writetype = CLIENTWRITE_HEADER | +- ((k->httpcode/100 == 1) ? CLIENTWRITE_1XX : 0); +- +- result = Curl_client_write(data, writetype, hd, hdlen); +- if(result) +- return result; +- +- result = Curl_bump_headersize(data, hdlen, FALSE); +- if(result) +- return result; +- /* We are done with this line. We reset because response +- * processing might switch to HTTP/2 and that might call us +- * directly again. */ +- Curl_dyn_reset(&data->state.headerb); +- +- data->req.deductheadercount = +- (100 <= k->httpcode && 199 >= k->httpcode)?data->req.headerbytecount:0; +- +- /* analyze the response to find out what to do */ +- result = http_on_response(data, buf, blen, &consumed); +- if(result) +- return result; +- *pconsumed += consumed; ++ result = http_rw_hd(data, Curl_dyn_ptr(&data->state.headerb), ++ Curl_dyn_len(&data->state.headerb), ++ buf, blen, &consumed); ++ /* We are done with this line. We reset because response ++ * processing might switch to HTTP/2 and that might call us ++ * directly again. */ ++ Curl_dyn_reset(&data->state.headerb); ++ if(consumed) { + blen -= consumed; + buf += consumed; +- +- if(!k->header || !blen) +- goto out; /* exit header line loop */ +- +- continue; +- } +- +- /* +- * Checks for special headers coming up. +- */ +- +- writetype = CLIENTWRITE_HEADER; +- if(!k->headerline++) { +- /* This is the first header, it MUST be the error code line +- or else we consider this to be the body right away! */ +- bool fine_statusline = FALSE; +- +- k->httpversion = 0; /* Don't know yet */ +- if(conn->handler->protocol & PROTO_FAMILY_HTTP) { +- /* +- * https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2 +- * +- * The response code is always a three-digit number in HTTP as the spec +- * says. We allow any three-digit number here, but we cannot make +- * guarantees on future behaviors since it isn't within the protocol. +- */ +- char *p = hd; +- +- while(*p && ISBLANK(*p)) +- p++; +- if(!strncmp(p, "HTTP/", 5)) { +- p += 5; +- switch(*p) { +- case '1': +- p++; +- if((p[0] == '.') && (p[1] == '0' || p[1] == '1')) { +- if(ISBLANK(p[2])) { +- k->httpversion = 10 + (p[1] - '0'); +- p += 3; +- if(ISDIGIT(p[0]) && ISDIGIT(p[1]) && ISDIGIT(p[2])) { +- k->httpcode = (p[0] - '0') * 100 + (p[1] - '0') * 10 + +- (p[2] - '0'); +- p += 3; +- if(ISSPACE(*p)) +- fine_statusline = TRUE; +- } +- } +- } +- if(!fine_statusline) { +- failf(data, "Unsupported HTTP/1 subversion in response"); +- return CURLE_UNSUPPORTED_PROTOCOL; +- } +- break; +- case '2': +- case '3': +- if(!ISBLANK(p[1])) +- break; +- k->httpversion = (*p - '0') * 10; +- p += 2; +- if(ISDIGIT(p[0]) && ISDIGIT(p[1]) && ISDIGIT(p[2])) { +- k->httpcode = (p[0] - '0') * 100 + (p[1] - '0') * 10 + +- (p[2] - '0'); +- p += 3; +- if(!ISSPACE(*p)) +- break; +- fine_statusline = TRUE; +- } +- break; +- default: /* unsupported */ +- failf(data, "Unsupported HTTP version in response"); +- return CURLE_UNSUPPORTED_PROTOCOL; +- } +- } +- +- if(!fine_statusline) { +- /* If user has set option HTTP200ALIASES, +- compare header line against list of aliases +- */ +- statusline check = checkhttpprefix(data, hd, hdlen); +- if(check == STATUS_DONE) { +- fine_statusline = TRUE; +- k->httpcode = 200; +- k->httpversion = 10; +- } +- } +- } +- else if(conn->handler->protocol & CURLPROTO_RTSP) { +- char *p = hd; +- while(*p && ISBLANK(*p)) +- p++; +- if(!strncmp(p, "RTSP/", 5)) { +- p += 5; +- if(ISDIGIT(*p)) { +- p++; +- if((p[0] == '.') && ISDIGIT(p[1])) { +- if(ISBLANK(p[2])) { +- p += 3; +- if(ISDIGIT(p[0]) && ISDIGIT(p[1]) && ISDIGIT(p[2])) { +- k->httpcode = (p[0] - '0') * 100 + (p[1] - '0') * 10 + +- (p[2] - '0'); +- p += 3; +- if(ISSPACE(*p)) { +- fine_statusline = TRUE; +- k->httpversion = 11; /* RTSP acts like HTTP 1.1 */ +- } +- } +- } +- } +- } +- if(!fine_statusline) +- return CURLE_WEIRD_SERVER_REPLY; +- } +- } +- +- if(fine_statusline) { +- result = Curl_http_statusline(data, conn); +- if(result) +- return result; +- writetype |= CLIENTWRITE_STATUS; +- } +- else { +- k->header = FALSE; /* this is not a header line */ +- break; +- } ++ *pconsumed += consumed; + } +- +- result = verify_header(data); +- if(result) +- return result; +- +- result = Curl_http_header(data, conn, hd, hdlen); +- if(result) +- return result; +- +- /* +- * Taken in one (more) header. Write it to the client. +- */ +- Curl_debug(data, CURLINFO_HEADER_IN, hd, hdlen); +- +- if(k->httpcode/100 == 1) +- writetype |= CLIENTWRITE_1XX; +- result = Curl_client_write(data, writetype, hd, hdlen); +- if(result) +- return result; +- +- result = Curl_bump_headersize(data, hdlen, FALSE); + if(result) + return result; +- +- Curl_dyn_reset(&data->state.headerb); + } +- while(blen); + + /* We might have reached the end of the header part here, but + there might be a non-header part left in the end of the read +@@ -3935,6 +3925,22 @@ static CURLcode http_rw_headers(struct Curl_easy *data, + return CURLE_OK; + } + ++CURLcode Curl_http_write_resp_hd(struct Curl_easy *data, ++ const char *hd, size_t hdlen, ++ bool is_eos) ++{ ++ CURLcode result; ++ size_t consumed; ++ char tmp = 0; ++ ++ result = http_rw_hd(data, hd, hdlen, &tmp, 0, &consumed); ++ if(!result && is_eos) { ++ result = Curl_client_write(data, (CLIENTWRITE_BODY|CLIENTWRITE_EOS), ++ &tmp, 0); ++ } ++ return result; ++} ++ + /* + * HTTP protocol `write_resp` implementation. Will parse headers + * when not done yet and otherwise return without consuming data. +@@ -3950,11 +3956,8 @@ CURLcode Curl_http_write_resp_hds(struct Curl_easy *data, + else { + CURLcode result; + +- result = http_rw_headers(data, buf, blen, pconsumed); ++ result = http_parse_headers(data, buf, blen, pconsumed); + if(!result && !data->req.header) { +- /* we have successfully finished parsing the HEADERs */ +- result = Curl_http_firstwrite(data); +- + if(!data->req.no_body && Curl_dyn_len(&data->state.headerb)) { + /* leftover from parsing something that turned out not + * to be a header, only happens if we allow for +diff --git a/lib/http.h b/lib/http.h +index 047709f20fc5..47fae63c782b 100644 +--- a/lib/http.h ++++ b/lib/http.h +@@ -102,8 +102,8 @@ CURLcode Curl_http_target(struct Curl_easy *data, struct connectdata *conn, + struct dynbuf *req); + CURLcode Curl_http_statusline(struct Curl_easy *data, + struct connectdata *conn); +-CURLcode Curl_http_header(struct Curl_easy *data, struct connectdata *conn, +- char *headp, size_t hdlen); ++CURLcode Curl_http_header(struct Curl_easy *data, ++ const char *hd, size_t hdlen); + CURLcode Curl_transferencode(struct Curl_easy *data); + CURLcode Curl_http_req_set_reader(struct Curl_easy *data, + Curl_HttpReq httpreq, +@@ -134,6 +134,9 @@ int Curl_http_getsock_do(struct Curl_easy *data, struct connectdata *conn, + CURLcode Curl_http_write_resp(struct Curl_easy *data, + const char *buf, size_t blen, + bool is_eos); ++CURLcode Curl_http_write_resp_hd(struct Curl_easy *data, ++ const char *hd, size_t hdlen, ++ bool is_eos); + + /* These functions are in http.c */ + CURLcode Curl_http_input_auth(struct Curl_easy *data, bool proxy, +diff --git a/lib/http2.c b/lib/http2.c +index fb097c51bbfa..ca224fd6670c 100644 +--- a/lib/http2.c ++++ b/lib/http2.c +@@ -127,6 +127,7 @@ struct cf_h2_ctx { + struct bufq inbufq; /* network input */ + struct bufq outbufq; /* network output */ + struct bufc_pool stream_bufcp; /* spares for stream buffers */ ++ struct dynbuf scratch; /* scratch buffer for temp use */ + + size_t drain_total; /* sum of all stream's UrlState drain */ + uint32_t max_concurrent_streams; +@@ -153,6 +154,7 @@ static void cf_h2_ctx_clear(struct cf_h2_ctx *ctx) + Curl_bufq_free(&ctx->inbufq); + Curl_bufq_free(&ctx->outbufq); + Curl_bufcp_free(&ctx->stream_bufcp); ++ Curl_dyn_free(&ctx->scratch); + memset(ctx, 0, sizeof(*ctx)); + ctx->call_data = save; + } +@@ -408,6 +410,7 @@ static CURLcode cf_h2_ctx_init(struct Curl_cfilter *cf, + Curl_bufcp_init(&ctx->stream_bufcp, H2_CHUNK_SIZE, H2_STREAM_POOL_SPARES); + Curl_bufq_initp(&ctx->inbufq, &ctx->stream_bufcp, H2_NW_RECV_CHUNKS, 0); + Curl_bufq_initp(&ctx->outbufq, &ctx->stream_bufcp, H2_NW_SEND_CHUNKS, 0); ++ Curl_dyn_init(&ctx->scratch, CURL_MAX_HTTP_HEADER); + ctx->last_stream_id = 2147483647; + + rc = nghttp2_session_callbacks_new(&cbs); +@@ -945,14 +948,6 @@ static int push_promise(struct Curl_cfilter *cf, + return rv; + } + +-static CURLcode recvbuf_write_hds(struct Curl_cfilter *cf, +- struct Curl_easy *data, +- const char *buf, size_t blen) +-{ +- (void)cf; +- return Curl_xfer_write_resp(data, (char *)buf, blen, FALSE); +-} +- + static CURLcode on_stream_frame(struct Curl_cfilter *cf, + struct Curl_easy *data, + const nghttp2_frame *frame) +@@ -1008,7 +1003,7 @@ static CURLcode on_stream_frame(struct Curl_cfilter *cf, + stream->status_code = -1; + } + +- result = recvbuf_write_hds(cf, data, STRCONST("\r\n")); ++ result = Curl_xfer_write_resp_hd(data, STRCONST("\r\n"), stream->closed); + if(result) + return result; + +@@ -1359,6 +1354,7 @@ static int on_header(nghttp2_session *session, const nghttp2_frame *frame, + void *userp) + { + struct Curl_cfilter *cf = userp; ++ struct cf_h2_ctx *ctx = cf->ctx; + struct h2_stream_ctx *stream; + struct Curl_easy *data_s; + int32_t stream_id = frame->hd.stream_id; +@@ -1468,14 +1464,15 @@ static int on_header(nghttp2_session *session, const nghttp2_frame *frame, + result = Curl_headers_push(data_s, buffer, CURLH_PSEUDO); + if(result) + return NGHTTP2_ERR_CALLBACK_FAILURE; +- result = recvbuf_write_hds(cf, data_s, STRCONST("HTTP/2 ")); +- if(result) +- return NGHTTP2_ERR_CALLBACK_FAILURE; +- result = recvbuf_write_hds(cf, data_s, (const char *)value, valuelen); +- if(result) +- return NGHTTP2_ERR_CALLBACK_FAILURE; +- /* the space character after the status code is mandatory */ +- result = recvbuf_write_hds(cf, data_s, STRCONST(" \r\n")); ++ Curl_dyn_reset(&ctx->scratch); ++ result = Curl_dyn_addn(&ctx->scratch, STRCONST("HTTP/2 ")); ++ if(!result) ++ result = Curl_dyn_addn(&ctx->scratch, value, valuelen); ++ if(!result) ++ result = Curl_dyn_addn(&ctx->scratch, STRCONST(" \r\n")); ++ if(!result) ++ result = Curl_xfer_write_resp_hd(data_s, Curl_dyn_ptr(&ctx->scratch), ++ Curl_dyn_len(&ctx->scratch), FALSE); + if(result) + return NGHTTP2_ERR_CALLBACK_FAILURE; + /* if we receive data for another handle, wake that up */ +@@ -1490,16 +1487,17 @@ static int on_header(nghttp2_session *session, const nghttp2_frame *frame, + /* nghttp2 guarantees that namelen > 0, and :status was already + received, and this is not pseudo-header field . */ + /* convert to an HTTP1-style header */ +- result = recvbuf_write_hds(cf, data_s, (const char *)name, namelen); +- if(result) +- return NGHTTP2_ERR_CALLBACK_FAILURE; +- result = recvbuf_write_hds(cf, data_s, STRCONST(": ")); +- if(result) +- return NGHTTP2_ERR_CALLBACK_FAILURE; +- result = recvbuf_write_hds(cf, data_s, (const char *)value, valuelen); +- if(result) +- return NGHTTP2_ERR_CALLBACK_FAILURE; +- result = recvbuf_write_hds(cf, data_s, STRCONST("\r\n")); ++ Curl_dyn_reset(&ctx->scratch); ++ result = Curl_dyn_addn(&ctx->scratch, (const char *)name, namelen); ++ if(!result) ++ result = Curl_dyn_addn(&ctx->scratch, STRCONST(": ")); ++ if(!result) ++ result = Curl_dyn_addn(&ctx->scratch, (const char *)value, valuelen); ++ if(!result) ++ result = Curl_dyn_addn(&ctx->scratch, STRCONST("\r\n")); ++ if(!result) ++ result = Curl_xfer_write_resp_hd(data_s, Curl_dyn_ptr(&ctx->scratch), ++ Curl_dyn_len(&ctx->scratch), FALSE); + if(result) + return NGHTTP2_ERR_CALLBACK_FAILURE; + /* if we receive data for another handle, wake that up */ +diff --git a/lib/imap.c b/lib/imap.c +index 0e013e740ae9..679bfae977f4 100644 +--- a/lib/imap.c ++++ b/lib/imap.c +@@ -131,6 +131,7 @@ const struct Curl_handler Curl_handler_imap = { + ZERO_NULL, /* perform_getsock */ + imap_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_IMAP, /* defport */ +@@ -160,6 +161,7 @@ const struct Curl_handler Curl_handler_imaps = { + ZERO_NULL, /* perform_getsock */ + imap_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_IMAPS, /* defport */ +diff --git a/lib/ldap.c b/lib/ldap.c +index 394fd32d4a14..e2546aa59eca 100644 +--- a/lib/ldap.c ++++ b/lib/ldap.c +@@ -178,6 +178,7 @@ const struct Curl_handler Curl_handler_ldap = { + ZERO_NULL, /* perform_getsock */ + ZERO_NULL, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_LDAP, /* defport */ +@@ -206,6 +207,7 @@ const struct Curl_handler Curl_handler_ldaps = { + ZERO_NULL, /* perform_getsock */ + ZERO_NULL, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_LDAPS, /* defport */ +diff --git a/lib/mqtt.c b/lib/mqtt.c +index 9290da031ba3..35458648daee 100644 +--- a/lib/mqtt.c ++++ b/lib/mqtt.c +@@ -89,6 +89,7 @@ const struct Curl_handler Curl_handler_mqtt = { + ZERO_NULL, /* perform_getsock */ + ZERO_NULL, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_MQTT, /* defport */ +diff --git a/lib/openldap.c b/lib/openldap.c +index 85a37b818604..a5fac50577d3 100644 +--- a/lib/openldap.c ++++ b/lib/openldap.c +@@ -131,6 +131,7 @@ const struct Curl_handler Curl_handler_ldap = { + ZERO_NULL, /* perform_getsock */ + oldap_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_LDAP, /* defport */ +@@ -159,6 +160,7 @@ const struct Curl_handler Curl_handler_ldaps = { + ZERO_NULL, /* perform_getsock */ + oldap_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_LDAPS, /* defport */ +diff --git a/lib/pop3.c b/lib/pop3.c +index 993b2e1c7f52..85c12cbf2cd3 100644 +--- a/lib/pop3.c ++++ b/lib/pop3.c +@@ -126,6 +126,7 @@ const struct Curl_handler Curl_handler_pop3 = { + ZERO_NULL, /* perform_getsock */ + pop3_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_POP3, /* defport */ +@@ -155,6 +156,7 @@ const struct Curl_handler Curl_handler_pop3s = { + ZERO_NULL, /* perform_getsock */ + pop3_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_POP3S, /* defport */ +@@ -1450,7 +1452,7 @@ static CURLcode pop3_parse_custom_request(struct Curl_easy *data) + * This function scans the body after the end-of-body and writes everything + * until the end is found. + */ +-CURLcode Curl_pop3_write(struct Curl_easy *data, char *str, size_t nread) ++CURLcode Curl_pop3_write(struct Curl_easy *data, const char *str, size_t nread) + { + /* This code could be made into a special function in the handler struct */ + CURLcode result = CURLE_OK; +diff --git a/lib/pop3.h b/lib/pop3.h +index 83f0f831e6c1..e8e98db04b62 100644 +--- a/lib/pop3.h ++++ b/lib/pop3.h +@@ -92,6 +92,7 @@ extern const struct Curl_handler Curl_handler_pop3s; + + /* This function scans the body after the end-of-body and writes everything + * until the end is found */ +-CURLcode Curl_pop3_write(struct Curl_easy *data, char *str, size_t nread); ++CURLcode Curl_pop3_write(struct Curl_easy *data, ++ const char *str, size_t nread); + + #endif /* HEADER_CURL_POP3_H */ +diff --git a/lib/request.c b/lib/request.c +index 40515a990754..d1605d32a165 100644 +--- a/lib/request.c ++++ b/lib/request.c +@@ -266,7 +266,7 @@ static CURLcode req_set_upload_done(struct Curl_easy *data) + else if(data->req.writebytecount) + infof(data, "upload completely sent off: %" CURL_FORMAT_CURL_OFF_T + " bytes", data->req.writebytecount); +- else ++ else if(!data->req.download_done) + infof(data, Curl_creader_total_length(data)? + "We are completely uploaded and fine" : + "Request completely sent off"); +diff --git a/lib/rtsp.c b/lib/rtsp.c +index 7251c062b1b0..ab7fda4c0636 100644 +--- a/lib/rtsp.c ++++ b/lib/rtsp.c +@@ -93,7 +93,7 @@ static int rtsp_getsock_do(struct Curl_easy *data, struct connectdata *conn, + static + CURLcode rtp_client_write(struct Curl_easy *data, const char *ptr, size_t len); + static +-CURLcode rtsp_parse_transport(struct Curl_easy *data, char *transport); ++CURLcode rtsp_parse_transport(struct Curl_easy *data, const char *transport); + + + /* +@@ -114,6 +114,7 @@ const struct Curl_handler Curl_handler_rtsp = { + ZERO_NULL, /* perform_getsock */ + rtsp_disconnect, /* disconnect */ + rtsp_rtp_write_resp, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + rtsp_conncheck, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_RTSP, /* defport */ +@@ -913,12 +914,12 @@ CURLcode rtp_client_write(struct Curl_easy *data, const char *ptr, size_t len) + return CURLE_OK; + } + +-CURLcode Curl_rtsp_parseheader(struct Curl_easy *data, char *header) ++CURLcode Curl_rtsp_parseheader(struct Curl_easy *data, const char *header) + { + if(checkprefix("CSeq:", header)) { + long CSeq = 0; + char *endp; +- char *p = &header[5]; ++ const char *p = &header[5]; + while(ISBLANK(*p)) + p++; + CSeq = strtol(p, &endp, 10); +@@ -933,8 +934,7 @@ CURLcode Curl_rtsp_parseheader(struct Curl_easy *data, char *header) + } + } + else if(checkprefix("Session:", header)) { +- char *start; +- char *end; ++ const char *start, *end; + size_t idlen; + + /* Find the first non-space letter */ +@@ -989,14 +989,13 @@ CURLcode Curl_rtsp_parseheader(struct Curl_easy *data, char *header) + } + + static +-CURLcode rtsp_parse_transport(struct Curl_easy *data, char *transport) ++CURLcode rtsp_parse_transport(struct Curl_easy *data, const char *transport) + { + /* If we receive multiple Transport response-headers, the linterleaved + channels of each response header is recorded and used together for + subsequent data validity checks.*/ + /* e.g.: ' RTP/AVP/TCP;unicast;interleaved=5-6' */ +- char *start; +- char *end; ++ const char *start, *end; + start = transport; + while(start && *start) { + while(*start && ISBLANK(*start) ) +@@ -1005,7 +1004,7 @@ CURLcode rtsp_parse_transport(struct Curl_easy *data, char *transport) + if(checkprefix("interleaved=", start)) { + long chan1, chan2, chan; + char *endp; +- char *p = start + 12; ++ const char *p = start + 12; + chan1 = strtol(p, &endp, 10); + if(p != endp && chan1 >= 0 && chan1 <= 255) { + unsigned char *rtp_channel_mask = data->state.rtp_channel_mask; +diff --git a/lib/rtsp.h b/lib/rtsp.h +index 237b80f809fa..b1ffa5c7ea65 100644 +--- a/lib/rtsp.h ++++ b/lib/rtsp.h +@@ -31,7 +31,7 @@ + + extern const struct Curl_handler Curl_handler_rtsp; + +-CURLcode Curl_rtsp_parseheader(struct Curl_easy *data, char *header); ++CURLcode Curl_rtsp_parseheader(struct Curl_easy *data, const char *header); + + #else + /* disabled */ +diff --git a/lib/smb.c b/lib/smb.c +index 77d34e31c483..2ce6dbf7fc11 100644 +--- a/lib/smb.c ++++ b/lib/smb.c +@@ -273,6 +273,7 @@ const struct Curl_handler Curl_handler_smb = { + ZERO_NULL, /* perform_getsock */ + smb_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_SMB, /* defport */ +@@ -300,6 +301,7 @@ const struct Curl_handler Curl_handler_smbs = { + ZERO_NULL, /* perform_getsock */ + smb_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_SMBS, /* defport */ +diff --git a/lib/smtp.c b/lib/smtp.c +index 20763c0c823b..dc2f1c91804e 100644 +--- a/lib/smtp.c ++++ b/lib/smtp.c +@@ -132,6 +132,7 @@ const struct Curl_handler Curl_handler_smtp = { + ZERO_NULL, /* perform_getsock */ + smtp_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_SMTP, /* defport */ +@@ -161,6 +162,7 @@ const struct Curl_handler Curl_handler_smtps = { + ZERO_NULL, /* perform_getsock */ + smtp_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_SMTPS, /* defport */ +diff --git a/lib/telnet.c b/lib/telnet.c +index 56ee0855f0f8..b129ff520210 100644 +--- a/lib/telnet.c ++++ b/lib/telnet.c +@@ -187,6 +187,7 @@ const struct Curl_handler Curl_handler_telnet = { + ZERO_NULL, /* perform_getsock */ + ZERO_NULL, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_TELNET, /* defport */ +diff --git a/lib/tftp.c b/lib/tftp.c +index ff2b7b7457b2..4667ae1e42d6 100644 +--- a/lib/tftp.c ++++ b/lib/tftp.c +@@ -182,6 +182,7 @@ const struct Curl_handler Curl_handler_tftp = { + ZERO_NULL, /* perform_getsock */ + tftp_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_TFTP, /* defport */ +diff --git a/lib/transfer.c b/lib/transfer.c +index ac4710ef5d3c..6e2067cd6a27 100644 +--- a/lib/transfer.c ++++ b/lib/transfer.c +@@ -1156,7 +1156,7 @@ void Curl_xfer_setup( + } + + CURLcode Curl_xfer_write_resp(struct Curl_easy *data, +- char *buf, size_t blen, ++ const char *buf, size_t blen, + bool is_eos) + { + CURLcode result = CURLE_OK; +@@ -1195,6 +1195,18 @@ CURLcode Curl_xfer_write_resp(struct Curl_easy *data, + return result; + } + ++CURLcode Curl_xfer_write_resp_hd(struct Curl_easy *data, ++ const char *hd0, size_t hdlen, bool is_eos) ++{ ++ if(data->conn->handler->write_resp_hd) { ++ /* protocol handlers offering this function take full responsibility ++ * for writing all received download data to the client. */ ++ return data->conn->handler->write_resp_hd(data, hd0, hdlen, is_eos); ++ } ++ /* No special handling by protocol handler, write as response bytes */ ++ return Curl_xfer_write_resp(data, hd0, hdlen, is_eos); ++} ++ + CURLcode Curl_xfer_write_done(struct Curl_easy *data, bool premature) + { + (void)premature; +diff --git a/lib/transfer.h b/lib/transfer.h +index e65b2b147215..ad0f3a20cc95 100644 +--- a/lib/transfer.h ++++ b/lib/transfer.h +@@ -62,12 +62,20 @@ bool Curl_meets_timecondition(struct Curl_easy *data, time_t timeofdoc); + * @param blen the amount of bytes in `buf` + * @param is_eos TRUE iff the connection indicates this to be the last + * bytes of the response +- * @param done on returnm, TRUE iff the response is complete + */ + CURLcode Curl_xfer_write_resp(struct Curl_easy *data, +- char *buf, size_t blen, ++ const char *buf, size_t blen, + bool is_eos); + ++/** ++ * Write a single "header" line from a server response. ++ * @param hd0 the 0-terminated, single header line ++ * @param hdlen the length of the header line ++ * @param is_eos TRUE iff this is the end of the response ++ */ ++CURLcode Curl_xfer_write_resp_hd(struct Curl_easy *data, ++ const char *hd0, size_t hdlen, bool is_eos); ++ + /* This sets up a forthcoming transfer */ + void Curl_xfer_setup(struct Curl_easy *data, + int sockindex, /* socket index to read from or -1 */ +diff --git a/lib/urldata.h b/lib/urldata.h +index 90f4d1bb574c..308c51c92379 100644 +--- a/lib/urldata.h ++++ b/lib/urldata.h +@@ -701,12 +701,18 @@ struct Curl_handler { + CURLcode (*disconnect)(struct Curl_easy *, struct connectdata *, + bool dead_connection); + +- /* If used, this function gets called from transfer.c:readwrite_data() to ++ /* If used, this function gets called from transfer.c to + allow the protocol to do extra handling in writing response to + the client. */ + CURLcode (*write_resp)(struct Curl_easy *data, const char *buf, size_t blen, + bool is_eos); + ++ /* If used, this function gets called from transfer.c to ++ allow the protocol to do extra handling in writing a single response ++ header line to the client. */ ++ CURLcode (*write_resp_hd)(struct Curl_easy *data, ++ const char *hd, size_t hdlen, bool is_eos); ++ + /* This function can perform various checks on the connection. See + CONNCHECK_* for more information about the checks that can be performed, + and CONNRESULT_* for the results that can be returned. */ +diff --git a/lib/vquic/curl_ngtcp2.c b/lib/vquic/curl_ngtcp2.c +index eea8e1261e75..74006b47277c 100644 +--- a/lib/vquic/curl_ngtcp2.c ++++ b/lib/vquic/curl_ngtcp2.c +@@ -130,6 +130,7 @@ struct cf_ngtcp2_ctx { + struct curltime handshake_at; /* time connect handshake finished */ + struct curltime reconnect_at; /* time the next attempt should start */ + struct bufc_pool stream_bufcp; /* chunk pool for streams */ ++ struct dynbuf scratch; /* temp buffer for header construction */ + size_t max_stream_window; /* max flow window for one stream */ + uint64_t max_idle_ms; /* max idle time for QUIC connection */ + int qlogfd; +@@ -765,12 +766,6 @@ static int cb_h3_stream_close(nghttp3_conn *conn, int64_t sid, + return 0; + } + +-static CURLcode write_resp_hds(struct Curl_easy *data, +- const char *buf, size_t blen) +-{ +- return Curl_xfer_write_resp(data, (char *)buf, blen, FALSE); +-} +- + static int cb_h3_recv_data(nghttp3_conn *conn, int64_t stream3_id, + const uint8_t *buf, size_t blen, + void *user_data, void *stream_user_data) +@@ -828,7 +828,7 @@ static int cb_h3_end_headers(nghttp3_con + if(!stream) + return 0; + /* add a CRLF only if we've received some headers */ +- result = write_resp_hds(data, "\r\n", 2); ++ result = Curl_xfer_write_resp_hd(data, STRCONST("\r\n"), stream->closed); + if(result) { + return -1; + } +@@ -848,6 +848,7 @@ static int cb_h3_recv_header(nghttp3_con + void *user_data, void *stream_user_data) + { + struct Curl_cfilter *cf = user_data; ++ struct cf_ngtcp2_ctx *ctx = cf->ctx; + nghttp3_vec h3name = nghttp3_rcbuf_get_buf(name); + nghttp3_vec h3val = nghttp3_rcbuf_get_buf(value); + struct Curl_easy *data = stream_user_data; +@@ -864,17 +865,23 @@ static int cb_h3_recv_header(nghttp3_con + return 0; + + if(token == NGHTTP3_QPACK_TOKEN__STATUS) { +- char line[14]; /* status line is always 13 characters long */ +- size_t ncopy; + + result = Curl_http_decode_status(&stream->status_code, + (const char *)h3val.base, h3val.len); + if(result) + return -1; +- ncopy = msnprintf(line, sizeof(line), "HTTP/3 %03d \r\n", +- stream->status_code); +- CURL_TRC_CF(data, cf, "[%" PRId64 "] status: %s", stream_id, line); +- result = write_resp_hds(data, line, ncopy); ++ Curl_dyn_reset(&ctx->scratch); ++ result = Curl_dyn_addn(&ctx->scratch, STRCONST("HTTP/3 ")); ++ if(!result) ++ result = Curl_dyn_addn(&ctx->scratch, ++ (const char *)h3val.base, h3val.len); ++ if(!result) ++ result = Curl_dyn_addn(&ctx->scratch, STRCONST(" \r\n")); ++ if(!result) ++ result = Curl_xfer_write_resp_hd(data, Curl_dyn_ptr(&ctx->scratch), ++ Curl_dyn_len(&ctx->scratch), FALSE); ++ CURL_TRC_CF(data, cf, "[%" CURL_PRId64 "] status: %s", ++ stream_id, Curl_dyn_ptr(&ctx->scratch)); + if(result) { + return -1; + } +@@ -884,19 +891,19 @@ static int cb_h3_recv_header(nghttp3_con + CURL_TRC_CF(data, cf, "[%" PRId64 "] header: %.*s: %.*s", + stream_id, (int)h3name.len, h3name.base, + (int)h3val.len, h3val.base); +- result = write_resp_hds(data, (const char *)h3name.base, h3name.len); +- if(result) { +- return -1; +- } +- result = write_resp_hds(data, ": ", 2); +- if(result) { +- return -1; +- } +- result = write_resp_hds(data, (const char *)h3val.base, h3val.len); +- if(result) { +- return -1; +- } +- result = write_resp_hds(data, "\r\n", 2); ++ Curl_dyn_reset(&ctx->scratch); ++ result = Curl_dyn_addn(&ctx->scratch, ++ (const char *)h3name.base, h3name.len); ++ if(!result) ++ result = Curl_dyn_addn(&ctx->scratch, STRCONST(": ")); ++ if(!result) ++ result = Curl_dyn_addn(&ctx->scratch, ++ (const char *)h3val.base, h3val.len); ++ if(!result) ++ result = Curl_dyn_addn(&ctx->scratch, STRCONST("\r\n")); ++ if(!result) ++ result = Curl_xfer_write_resp_hd(data, Curl_dyn_ptr(&ctx->scratch), ++ Curl_dyn_len(&ctx->scratch), FALSE); + if(result) { + return -1; + } +@@ -1846,6 +1853,7 @@ static void cf_ngtcp2_ctx_clear(struct c + if(ctx->qconn) + ngtcp2_conn_del(ctx->qconn); + Curl_bufcp_free(&ctx->stream_bufcp); ++ Curl_dyn_free(&ctx->scratch); + Curl_ssl_peer_cleanup(&ctx->peer); + + memset(ctx, 0, sizeof(*ctx)); +@@ -1948,6 +1956,7 @@ static CURLcode cf_connect_start(struct + ctx->max_idle_ms = CURL_QUIC_MAX_IDLE_MS; + Curl_bufcp_init(&ctx->stream_bufcp, H3_STREAM_CHUNK_SIZE, + H3_STREAM_POOL_SPARES); ++ Curl_dyn_init(&ctx->scratch, CURL_MAX_HTTP_HEADER); + + result = Curl_ssl_peer_init(&ctx->peer, cf); + if(result) +diff --git a/lib/vssh/libssh.c b/lib/vssh/libssh.c +index da7ce6ad9890..d6ba987f1c7c 100644 +--- a/lib/vssh/libssh.c ++++ b/lib/vssh/libssh.c +@@ -162,6 +162,7 @@ const struct Curl_handler Curl_handler_scp = { + myssh_getsock, /* perform_getsock */ + scp_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_SSH, /* defport */ +@@ -189,6 +190,7 @@ const struct Curl_handler Curl_handler_sftp = { + myssh_getsock, /* perform_getsock */ + sftp_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_SSH, /* defport */ +diff --git a/lib/vssh/libssh2.c b/lib/vssh/libssh2.c +index 7d8d5f46571e..6c5704b6a4ec 100644 +--- a/lib/vssh/libssh2.c ++++ b/lib/vssh/libssh2.c +@@ -139,6 +139,7 @@ const struct Curl_handler Curl_handler_scp = { + ssh_getsock, /* perform_getsock */ + scp_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ssh_attach, /* attach */ + PORT_SSH, /* defport */ +@@ -168,6 +169,7 @@ const struct Curl_handler Curl_handler_sftp = { + ssh_getsock, /* perform_getsock */ + sftp_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ssh_attach, /* attach */ + PORT_SSH, /* defport */ +diff --git a/lib/vssh/wolfssh.c b/lib/vssh/wolfssh.c +index 11275910a1f9..6a5aed88f74f 100644 +--- a/lib/vssh/wolfssh.c ++++ b/lib/vssh/wolfssh.c +@@ -94,6 +94,7 @@ const struct Curl_handler Curl_handler_scp = { + wssh_getsock, /* perform_getsock */ + wscp_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_SSH, /* defport */ +@@ -123,6 +124,7 @@ const struct Curl_handler Curl_handler_sftp = { + wssh_getsock, /* perform_getsock */ + wsftp_disconnect, /* disconnect */ + ZERO_NULL, /* write_resp */ ++ ZERO_NULL, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_SSH, /* defport */ +diff --git a/lib/ws.c b/lib/ws.c +index 907f64bac3f5..d6a83be90f64 100644 +--- a/lib/ws.c ++++ b/lib/ws.c +@@ -1199,6 +1199,7 @@ const struct Curl_handler Curl_handler_ws = { + ZERO_NULL, /* perform_getsock */ + ws_disconnect, /* disconnect */ + Curl_http_write_resp, /* write_resp */ ++ Curl_http_write_resp_hd, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_HTTP, /* defport */ +@@ -1224,6 +1225,7 @@ const struct Curl_handler Curl_handler_wss = { + ZERO_NULL, /* perform_getsock */ + ws_disconnect, /* disconnect */ + Curl_http_write_resp, /* write_resp */ ++ Curl_http_write_resp_hd, /* write_resp_hd */ + ZERO_NULL, /* connection_check */ + ZERO_NULL, /* attach connection */ + PORT_HTTPS, /* defport */ diff --git a/meta/recipes-support/curl/curl_8.7.1.bb b/meta/recipes-support/curl/curl_8.7.1.bb index 9e37684b2c..d2c5682eaf 100644 --- a/meta/recipes-support/curl/curl_8.7.1.bb +++ b/meta/recipes-support/curl/curl_8.7.1.bb @@ -32,6 +32,8 @@ SRC_URI = " \ file://CVE-2025-14819.patch \ file://CVE-2025-15079.patch \ file://CVE-2025-15224.patch \ + file://lib-add-Curl_xfer_write_resp_hd.patch \ + file://http2-ngtcp2-pass-CURLcode-errors-from-callbacks.patch \ " SRC_URI:append:class-nativesdk = " \