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
This commit is contained in:
Stefan Eissing 2026-01-07 15:07:13 +01:00 committed by Daniel Stenberg
parent 3b1c2a1510
commit 0f042efcb1
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
4 changed files with 66 additions and 48 deletions

View file

@ -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;
}

View file

@ -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;
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 */
}
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->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->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);
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;
}

View file

@ -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));

View file

@ -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));