From: Stefan Eissing Date: Wed, 7 Jan 2026 14:07:13 +0000 (+0100) Subject: h2+h3: align stream close handling X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0f042efcb156058b14fca87328ab0d356f3426f0;p=thirdparty%2Fcurl.git h2+h3: align stream close handling For HTTP/2, add error code description to close failures. For HTTP/3, add special handling like in HTTP/2 when streams have been rejected or an error comes during the response body and we are not interested in the body. Closes #20207 --- diff --git a/lib/cf-h2-proxy.c b/lib/cf-h2-proxy.c index 0a7f4b4872..c600102bac 100644 --- a/lib/cf-h2-proxy.c +++ b/lib/cf-h2-proxy.c @@ -588,6 +588,10 @@ static int proxy_h2_on_frame_recv(nghttp2_session *session, drain_tunnel(cf, data, &ctx->tunnel); } break; + case NGHTTP2_RST_STREAM: + if(frame->rst_stream.error_code) + ctx->tunnel.reset = TRUE; + break; default: break; } @@ -747,6 +751,8 @@ static int proxy_h2_on_stream_close(nghttp2_session *session, stream_id, nghttp2_http2_strerror(error_code), error_code); ctx->tunnel.closed = TRUE; ctx->tunnel.error = error_code; + if(error_code) + ctx->tunnel.reset = TRUE; return 0; } @@ -1223,20 +1229,11 @@ static CURLcode h2_handle_tunnel_close(struct Curl_cfilter *cf, struct cf_h2_proxy_ctx *ctx = cf->ctx; *pnread = 0; - if(ctx->tunnel.error == NGHTTP2_REFUSED_STREAM) { - CURL_TRC_CF(data, cf, "[%d] REFUSED_STREAM, try again on a new " - "connection", ctx->tunnel.stream_id); - failf(data, "proxy server refused HTTP/2 stream"); - return CURLE_RECV_ERROR; /* trigger Curl_retry_request() later */ - } - else if(ctx->tunnel.error != NGHTTP2_NO_ERROR) { - failf(data, "HTTP/2 stream %u was not closed cleanly: %s (err %u)", - ctx->tunnel.stream_id, nghttp2_http2_strerror(ctx->tunnel.error), - ctx->tunnel.error); - return CURLE_HTTP2_STREAM; - } - else if(ctx->tunnel.reset) { - failf(data, "HTTP/2 stream %u was reset", ctx->tunnel.stream_id); + if(ctx->tunnel.error) { + failf(data, "HTTP/2 stream %" PRIu32 " reset by %s (error 0x%" PRIx32 + " %s)", ctx->tunnel.stream_id, + ctx->tunnel.reset ? "server" : "curl", + ctx->tunnel.error, nghttp2_http2_strerror(ctx->tunnel.error)); return CURLE_RECV_ERROR; } diff --git a/lib/http2.c b/lib/http2.c index 1864e37924..ac5a070f17 100644 --- a/lib/http2.c +++ b/lib/http2.c @@ -146,6 +146,7 @@ struct h2_stream_ctx { BIT(resp_hds_complete); /* we have a complete, final response */ BIT(closed); /* TRUE on stream close */ BIT(reset); /* TRUE on stream reset */ + BIT(reset_by_server); /* TRUE on stream reset by server */ BIT(close_handled); /* TRUE if stream closure is handled by libcurl */ BIT(bodystarted); BIT(body_eos); /* the complete body has been added to `sendbuf` and @@ -1011,10 +1012,8 @@ static CURLcode on_stream_frame(struct Curl_cfilter *cf, } break; case NGHTTP2_RST_STREAM: - stream->closed = TRUE; - if(frame->rst_stream.error_code) { - stream->reset = TRUE; - } + if(frame->rst_stream.error_code) + stream->reset_by_server = TRUE; Curl_multi_mark_dirty(data); break; case NGHTTP2_WINDOW_UPDATE: @@ -1332,9 +1331,8 @@ static int on_stream_close(nghttp2_session *session, int32_t stream_id, stream->closed = TRUE; stream->error = error_code; - if(stream->error) { + if(stream->error) stream->reset = TRUE; - } if(stream->error) CURL_TRC_CF(data_s, cf, "[%d] RESET: %s (err %d)", @@ -1686,36 +1684,31 @@ static CURLcode http2_handle_stream_close(struct Curl_cfilter *cf, CURLcode result; *pnlen = 0; - if(stream->error == NGHTTP2_REFUSED_STREAM) { - CURL_TRC_CF(data, cf, "[%d] REFUSED_STREAM, try again on a new " - "connection", stream->id); - connclose(cf->conn, "REFUSED_STREAM"); /* do not use this anymore */ - data->state.refused_stream = TRUE; - return CURLE_RECV_ERROR; /* trigger Curl_retry_request() later */ - } - else if(stream->error != NGHTTP2_NO_ERROR) { - if(stream->resp_hds_complete && data->req.no_body) { - CURL_TRC_CF(data, cf, "[%d] error after response headers, but we did " - "not want a body anyway, ignore: %s (err %u)", - stream->id, nghttp2_http2_strerror(stream->error), - stream->error); - stream->close_handled = TRUE; - return CURLE_OK; - } - failf(data, "HTTP/2 stream %u was not closed cleanly: %s (err %u)", - stream->id, nghttp2_http2_strerror(stream->error), - stream->error); - return CURLE_HTTP2_STREAM; - } - else if(stream->reset) { - failf(data, "HTTP/2 stream %u was reset", stream->id); - return data->req.bytecount ? CURLE_PARTIAL_FILE : CURLE_HTTP2; - } - - if(!stream->bodystarted) { - failf(data, "HTTP/2 stream %u was closed cleanly, but before getting " - " all response header fields, treated as error", - stream->id); + if(stream->reset) { + if(stream->error == NGHTTP2_REFUSED_STREAM) { + infof(data, "HTTP/2 stream %d refused by server, try again on a new " + "connection", stream->id); + connclose(cf->conn, "REFUSED_STREAM"); /* do not use this anymore */ + data->state.refused_stream = TRUE; + return CURLE_RECV_ERROR; /* trigger Curl_retry_request() later */ + } + else if(stream->resp_hds_complete && data->req.no_body) { + CURL_TRC_CF(data, cf, "[%d] error after response headers, but we did " + "not want a body anyway, ignore: %s (err %u)", + stream->id, nghttp2_http2_strerror(stream->error), + stream->error); + stream->close_handled = TRUE; + return CURLE_OK; + } + failf(data, "HTTP/2 stream %" PRIu32 " reset by %s (error 0x%" PRIx32 + " %s)", stream->id, stream->reset_by_server ? "server" : "curl", + stream->error, nghttp2_http2_strerror(stream->error)); + return stream->error ? CURLE_HTTP2_STREAM : + (data->req.bytecount ? CURLE_PARTIAL_FILE : CURLE_HTTP2); + } + else if(!stream->bodystarted) { + failf(data, "HTTP/2 stream %d was closed cleanly, but before getting " + " all response header fields, treated as error", stream->id); return CURLE_HTTP2_STREAM; } diff --git a/lib/vquic/curl_ngtcp2.c b/lib/vquic/curl_ngtcp2.c index ce5ac6eeb3..106776a2a4 100644 --- a/lib/vquic/curl_ngtcp2.c +++ b/lib/vquic/curl_ngtcp2.c @@ -1373,6 +1373,20 @@ static CURLcode recv_closed_stream(struct Curl_cfilter *cf, (void)cf; *pnread = 0; if(stream->reset) { + if(stream->error3 == CURL_H3_ERR_REQUEST_REJECTED) { + infof(data, "HTTP/3 stream %" PRId64 " refused by server, try again " + "on a new connection", stream->id); + connclose(cf->conn, "REFUSED_STREAM"); /* do not use this anymore */ + data->state.refused_stream = TRUE; + return CURLE_RECV_ERROR; /* trigger Curl_retry_request() later */ + } + else if(stream->resp_hds_complete && data->req.no_body) { + CURL_TRC_CF(data, cf, "[%" PRId64 "] error after response headers, " + "but we did not want a body anyway, ignore error 0x%" + PRIx64 " %s", stream->id, stream->error3, + vquic_h3_err_str(stream->error3)); + return CURLE_OK; + } failf(data, "HTTP/3 stream %" PRId64 " reset by server (error 0x%" PRIx64 " %s)", stream->id, stream->error3, vquic_h3_err_str(stream->error3)); diff --git a/lib/vquic/curl_quiche.c b/lib/vquic/curl_quiche.c index f7daf28200..e289548598 100644 --- a/lib/vquic/curl_quiche.c +++ b/lib/vquic/curl_quiche.c @@ -855,6 +855,20 @@ static CURLcode recv_closed_stream(struct Curl_cfilter *cf, DEBUGASSERT(stream); *pnread = 0; if(stream->reset) { + if(stream->error3 == CURL_H3_ERR_REQUEST_REJECTED) { + infof(data, "HTTP/3 stream %" PRIu64 " refused by server, try again " + "on a new connection", stream->id); + connclose(cf->conn, "REFUSED_STREAM"); /* do not use this anymore */ + data->state.refused_stream = TRUE; + return CURLE_RECV_ERROR; /* trigger Curl_retry_request() later */ + } + else if(stream->resp_hds_complete && data->req.no_body) { + CURL_TRC_CF(data, cf, "[%" PRIu64 "] error after response headers, " + "but we did not want a body anyway, ignore error 0x%" + PRIx64 " %s", stream->id, stream->error3, + vquic_h3_err_str(stream->error3)); + return CURLE_OK; + } failf(data, "HTTP/3 stream %" PRId64 " reset by server (error 0x%" PRIx64 " %s)", stream->id, stream->error3, vquic_h3_err_str(stream->error3));