new file mode 100644
@@ -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):
+
new file mode 100644
@@ -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 */
@@ -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 = " \