* [OE-core][scarthgap][PATCH] curl: backport HTTP/2 and HTTP/3 error propagation fixes
@ 2026-04-10 10:59 Zahir Hussain
2026-04-21 14:46 ` [scarthgap][PATCH] " aszh07
2026-04-24 14:35 ` [OE-core][scarthgap][PATCH] " Yoann Congal
0 siblings, 2 replies; 4+ messages in thread
From: Zahir Hussain @ 2026-04-10 10:59 UTC (permalink / raw)
To: openembedded-core, zahir.basha; +Cc: mail2szahir
From: Zahir Hussain <zahir.basha@kpit.com>
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 <zahir.basha@kpit.com>
---
...-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 <stefan@eissing.org>
+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 <zahir.basha@kpit.com>
+---
+ 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 <stefan@eissing.org>
+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 <zahir.basha@kpit.com>
+---
+ 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 = " \
--
2.34.1
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [scarthgap][PATCH] curl: backport HTTP/2 and HTTP/3 error propagation fixes
2026-04-10 10:59 [OE-core][scarthgap][PATCH] curl: backport HTTP/2 and HTTP/3 error propagation fixes Zahir Hussain
@ 2026-04-21 14:46 ` aszh07
2026-04-21 15:08 ` [OE-core] " Yoann Congal
2026-04-24 14:35 ` [OE-core][scarthgap][PATCH] " Yoann Congal
1 sibling, 1 reply; 4+ messages in thread
From: aszh07 @ 2026-04-21 14:46 UTC (permalink / raw)
To: openembedded-core
[-- Attachment #1: Type: text/plain, Size: 54 bytes --]
Hi,
Could someone confirm the status of this patch?
[-- Attachment #2: Type: text/html, Size: 69 bytes --]
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [OE-core] [scarthgap][PATCH] curl: backport HTTP/2 and HTTP/3 error propagation fixes
2026-04-21 14:46 ` [scarthgap][PATCH] " aszh07
@ 2026-04-21 15:08 ` Yoann Congal
0 siblings, 0 replies; 4+ messages in thread
From: Yoann Congal @ 2026-04-21 15:08 UTC (permalink / raw)
To: mail2szahir, openembedded-core
On Tue Apr 21, 2026 at 4:46 PM CEST, aszh07 via lists.openembedded.org wrote:
> Hi,
> Could someone confirm the status of this patch?
It's in my review branch but I have yet to review it:
https://git.yoctoproject.org/poky-contrib/commit/?h=stable/scarthgap-nut&id=eb89bbf2bd7583669fcead099731153611b4ff75
Regards,
--
Yoann Congal
Smile ECS
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [OE-core][scarthgap][PATCH] curl: backport HTTP/2 and HTTP/3 error propagation fixes
2026-04-10 10:59 [OE-core][scarthgap][PATCH] curl: backport HTTP/2 and HTTP/3 error propagation fixes Zahir Hussain
2026-04-21 14:46 ` [scarthgap][PATCH] " aszh07
@ 2026-04-24 14:35 ` Yoann Congal
1 sibling, 0 replies; 4+ messages in thread
From: Yoann Congal @ 2026-04-24 14:35 UTC (permalink / raw)
To: mail2szahir, openembedded-core, zahir.basha
On Fri Apr 10, 2026 at 12:59 PM CEST, aszh07 via lists.openembedded.org wrote:
> From: Zahir Hussain <zahir.basha@kpit.com>
>
> 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 <zahir.basha@kpit.com>
> ---
> ...-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 <stefan@eissing.org>
> +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 <zahir.basha@kpit.com>
> +---
> + lib/http2.c | 64 +++++++++++++++++++++--------
> + lib/vquic/curl_ngtcp2.c | 73 +++++++++++++++++++++++-----------
> + tests/http/test_02_download.py | 28 +++++++++++++
> + 3 files changed, 125 insertions(+), 40 deletions(-)
> +
Hello,
> +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 {
> +
^
Here, this patch is missing a space on empty context lines.
> + 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;
> + }
> +
^ Also here
Finally, those are quite big patches. How can we be sure they won't
break anything?
Regards,
> ++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 <stefan@eissing.org>
> +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 <zahir.basha@kpit.com>
> +---
> + 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 = " \
--
Yoann Congal
Smile ECS
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-04-24 14:35 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-10 10:59 [OE-core][scarthgap][PATCH] curl: backport HTTP/2 and HTTP/3 error propagation fixes Zahir Hussain
2026-04-21 14:46 ` [scarthgap][PATCH] " aszh07
2026-04-21 15:08 ` [OE-core] " Yoann Congal
2026-04-24 14:35 ` [OE-core][scarthgap][PATCH] " Yoann Congal
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox