diff mbox series

[scarthgap] curl: backport HTTP/2 and HTTP/3 error propagation fixes

Message ID 20260410105939.924439-1-zahir.basha@partner.bmwgroup.com
State New
Headers show
Series [scarthgap] curl: backport HTTP/2 and HTTP/3 error propagation fixes | expand

Commit Message

Zahir Hussain April 10, 2026, 10:59 a.m. UTC
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 mbox series

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 = " \