]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
h2+h3: align stream close handling
authorStefan Eissing <stefan@eissing.org>
Wed, 7 Jan 2026 14:07:13 +0000 (15:07 +0100)
committerDaniel Stenberg <daniel@haxx.se>
Wed, 28 Jan 2026 08:39:30 +0000 (09:39 +0100)
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

lib/cf-h2-proxy.c
lib/http2.c
lib/vquic/curl_ngtcp2.c
lib/vquic/curl_quiche.c

index 0a7f4b4872d56bf59339b06acb888ed2664a6250..c600102bac2a12d24e9d18e0ad80caf9b470b2e8 100644 (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;
   }
 
index 1864e37924a89b72aff78a1adeeed39c49e29adb..ac5a070f1701c31f8f97f7755ee2fd8648c65b17 100644 (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;
-    }
-    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;
   }
 
index ce5ac6eeb3178658111f6ccb60f216382a69c27f..106776a2a4eb5d10afbf17ce7ee71196d598512b 100644 (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));
index f7daf282001c988e0b6eea8cb60b29b8261a6e5e..e289548598c54fa0fb024b5465130ef29b017713 100644 (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));