]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
quiche: use client writer
authorStefan Eissing <stefan@eissing.org>
Tue, 2 Dec 2025 12:42:29 +0000 (13:42 +0100)
committerDaniel Stenberg <daniel@haxx.se>
Tue, 2 Dec 2025 15:25:03 +0000 (16:25 +0100)
Instead of buffering response body data until it is received by the
transfer loop, write the response data directly to the client.

Use a connection wide scratch buffer to get the response body from
quiche. Eliminates need for maintaining individual buffers for each
stream.

Fixes #19803
Reported-by: Stanislav Fort
Closes #19806

lib/vquic/curl_quiche.c

index 45f606ec6182fa81a74eff440c5438c9b33194ab..913311085270fe91cee67d96319f91518feb4f85 100644 (file)
@@ -89,8 +89,9 @@ struct cf_quiche_ctx {
   uint8_t scid[QUICHE_MAX_CONN_ID_LEN];
   struct curltime started_at;        /* time the current attempt started */
   struct curltime handshake_at;      /* time connect handshake finished */
-  struct bufc_pool stream_bufcp;     /* chunk pool for streams */
   struct uint_hash streams;          /* hash `data->mid` to `stream_ctx` */
+  struct dynbuf scratch;             /* temp buffer for header construction */
+  struct bufq writebuf;              /* temp buffer for writing bodies */
   curl_off_t data_recvd;
   BIT(initialized);
   BIT(goaway);                       /* got GOAWAY from server */
@@ -119,9 +120,10 @@ static void cf_quiche_ctx_init(struct cf_quiche_ctx *ctx)
     debug_log_init = 1;
   }
 #endif
-  Curl_bufcp_init(&ctx->stream_bufcp, H3_STREAM_CHUNK_SIZE,
-                  H3_STREAM_POOL_SPARES);
+  curlx_dyn_init(&ctx->scratch, CURL_MAX_HTTP_HEADER);
   Curl_uint32_hash_init(&ctx->streams, 63, h3_stream_hash_free);
+  Curl_bufq_init2(&ctx->writebuf, H3_STREAM_CHUNK_SIZE, H3_STREAM_RECV_CHUNKS,
+                  BUFQ_OPT_SOFT_LIMIT);
   ctx->data_recvd = 0;
   ctx->initialized = TRUE;
 }
@@ -134,8 +136,9 @@ static void cf_quiche_ctx_free(struct cf_quiche_ctx *ctx)
     Curl_vquic_tls_cleanup(&ctx->tls);
     Curl_ssl_peer_cleanup(&ctx->peer);
     vquic_ctx_free(&ctx->q);
-    Curl_bufcp_free(&ctx->stream_bufcp);
     Curl_uint32_hash_destroy(&ctx->streams);
+    curlx_dyn_free(&ctx->scratch);
+    Curl_bufq_free(&ctx->writebuf);
   }
   curlx_free(ctx);
 }
@@ -168,9 +171,10 @@ static CURLcode cf_flush_egress(struct Curl_cfilter *cf,
  */
 struct h3_stream_ctx {
   uint64_t id;             /* HTTP/3 protocol stream identifier */
-  struct bufq recvbuf;     /* h3 response */
   struct h1_req_parser h1; /* h1 request parsing */
   uint64_t error3;         /* HTTP/3 stream error code */
+  int status_code;         /* HTTP status code */
+  CURLcode xfer_result;    /* result from cf_quiche_write_(hd/body) */
   BIT(opened);             /* TRUE after stream has been opened */
   BIT(closed);             /* TRUE on stream close */
   BIT(reset);              /* TRUE on stream reset */
@@ -182,7 +186,6 @@ struct h3_stream_ctx {
 
 static void h3_stream_ctx_free(struct h3_stream_ctx *stream)
 {
-  Curl_bufq_free(&stream->recvbuf);
   Curl_h1_req_parse_free(&stream->h1);
   curlx_free(stream);
 }
@@ -252,6 +255,7 @@ static bool cf_quiche_do_expire(struct Curl_cfilter *cf,
   (void)stream;
   (void)user_data;
   CURL_TRC_CF(sdata, cf, "conn closed, mark as dirty");
+  stream->xfer_result = CURLE_SEND_ERROR;
   Curl_multi_mark_dirty(sdata);
   return TRUE;
 }
@@ -270,8 +274,6 @@ static CURLcode h3_data_setup(struct Curl_cfilter *cf,
     return CURLE_OUT_OF_MEMORY;
 
   stream->id = -1;
-  Curl_bufq_initp(&stream->recvbuf, &ctx->stream_bufcp,
-                  H3_STREAM_RECV_CHUNKS, BUFQ_OPT_SOFT_LIMIT);
   Curl_h1_req_parse_init(&stream->h1, H1_PARSE_DEFAULT_MAX_LINE_LEN);
 
   if(!Curl_uint32_hash_set(&ctx->streams, data->mid, stream)) {
@@ -282,28 +284,38 @@ static CURLcode h3_data_setup(struct Curl_cfilter *cf,
   return CURLE_OK;
 }
 
+static void cf_quiche_stream_close(struct Curl_cfilter *cf,
+                                   struct Curl_easy *data,
+                                   struct h3_stream_ctx *stream)
+{
+  struct cf_quiche_ctx *ctx = cf->ctx;
+  CURLcode result;
+
+  if(ctx->qconn && !stream->closed) {
+    quiche_conn_stream_shutdown(ctx->qconn, stream->id,
+                                QUICHE_SHUTDOWN_READ, CURL_H3_NO_ERROR);
+    if(!stream->send_closed) {
+      quiche_conn_stream_shutdown(ctx->qconn, stream->id,
+                                  QUICHE_SHUTDOWN_WRITE, CURL_H3_NO_ERROR);
+      stream->send_closed = TRUE;
+    }
+    stream->closed = TRUE;
+    result = cf_flush_egress(cf, data);
+    if(result)
+      CURL_TRC_CF(data, cf, "[%" PRIu64 "] stream close, flush egress -> %d",
+                  stream->id, result);
+  }
+}
+
 static void h3_data_done(struct Curl_cfilter *cf, struct Curl_easy *data)
 {
   struct cf_quiche_ctx *ctx = cf->ctx;
   struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, data);
-  CURLcode result;
 
   (void)cf;
   if(stream) {
     CURL_TRC_CF(data, cf, "[%" PRIu64 "] easy handle is done", stream->id);
-    if(ctx->qconn && !stream->closed) {
-      quiche_conn_stream_shutdown(ctx->qconn, stream->id,
-                                  QUICHE_SHUTDOWN_READ, CURL_H3_NO_ERROR);
-      if(!stream->send_closed) {
-        quiche_conn_stream_shutdown(ctx->qconn, stream->id,
-                                    QUICHE_SHUTDOWN_WRITE, CURL_H3_NO_ERROR);
-        stream->send_closed = TRUE;
-      }
-      stream->closed = TRUE;
-      result = cf_flush_egress(cf, data);
-      if(result)
-        CURL_TRC_CF(data, cf, "data_done, flush egress -> %d", result);
-    }
+    cf_quiche_stream_close(cf, data, stream);
     Curl_uint32_hash_remove(&ctx->streams, data->mid);
   }
 }
@@ -316,39 +328,29 @@ static void cf_quiche_expire_conn_closed(struct Curl_cfilter *cf,
   cf_quiche_for_all_streams(cf, data->multi, cf_quiche_do_expire, NULL);
 }
 
-/*
- * write_resp_raw() copies response data in raw format to the `data`'s
- * receive buffer. If not enough space is available, it appends to the
- * `data`'s overflow buffer.
- */
-static CURLcode write_resp_raw(struct Curl_cfilter *cf,
+static void cf_quiche_write_hd(struct Curl_cfilter *cf,
                                struct Curl_easy *data,
-                               const void *mem, size_t memlen)
+                               struct h3_stream_ctx *stream,
+                               const char *buf, size_t blen, bool eos)
 {
-  struct cf_quiche_ctx *ctx = cf->ctx;
-  struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, data);
-  CURLcode result = CURLE_OK;
-  size_t nwritten;
-
-  (void)cf;
-  if(!stream)
-    return CURLE_RECV_ERROR;
-  result = Curl_bufq_write(&stream->recvbuf, mem, memlen, &nwritten);
-  if(result)
-    return result;
-
-  if(nwritten < memlen) {
-    /* This MUST not happen. Our recbuf is dimensioned to hold the
-     * full max_stream_window and then some for this reason. */
-    DEBUGASSERT(0);
-    return CURLE_RECV_ERROR;
+  /* This function returns no error intentionally, but records
+   * the result at the stream, skipping further writes once the
+   * `result` of the transfer is known.
+   * The stream is subsequently cancelled "higher up" in the filter's
+   * send/recv callbacks. Closing the stream here leads to SEND/RECV
+   * errors in other places that then overwrite the transfer's result. */
+  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, "[%" PRId64 "] error %d writing %zu "
+                  "bytes of headers", stream->id, stream->xfer_result, blen);
   }
-  return result;
 }
 
 struct cb_ctx {
   struct Curl_cfilter *cf;
   struct Curl_easy *data;
+  struct h3_stream_ctx *stream;
 };
 
 static int cb_each_header(uint8_t *name, size_t name_len,
@@ -356,39 +358,59 @@ static int cb_each_header(uint8_t *name, size_t name_len,
                           void *argp)
 {
   struct cb_ctx *x = argp;
-  struct cf_quiche_ctx *ctx = x->cf->ctx;
-  struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, x->data);
+  struct Curl_cfilter *cf = x->cf;
+  struct Curl_easy *data = x->data;
+  struct h3_stream_ctx *stream = x->stream;
+  struct cf_quiche_ctx *ctx = cf->ctx;
   CURLcode result;
 
-  if(!stream)
-    return CURLE_OK;
+  if(!stream || stream->xfer_result)
+    return 1; /* abort iteration */
 
   if((name_len == 7) && !strncmp(HTTP_PSEUDO_STATUS, (char *)name, 7)) {
-    CURL_TRC_CF(x->data, x->cf, "[%" PRIu64 "] status: %.*s",
-                stream->id, (int)value_len, value);
-    result = write_resp_raw(x->cf, x->data, "HTTP/3 ", sizeof("HTTP/3 ") - 1);
+    curlx_dyn_reset(&ctx->scratch);
+    result = Curl_http_decode_status(&stream->status_code,
+                                     (const char *)value, value_len);
+    if(!result)
+      result = curlx_dyn_addn(&ctx->scratch, STRCONST("HTTP/3 "));
+    if(!result)
+      result = curlx_dyn_addn(&ctx->scratch,
+                              (const char *)value, value_len);
     if(!result)
-      result = write_resp_raw(x->cf, x->data, value, value_len);
+      result = curlx_dyn_addn(&ctx->scratch, STRCONST(" \r\n"));
     if(!result)
-      result = write_resp_raw(x->cf, x->data, " \r\n", 3);
+      cf_quiche_write_hd(cf, data, stream, curlx_dyn_ptr(&ctx->scratch),
+                         curlx_dyn_len(&ctx->scratch), FALSE);
+    CURL_TRC_CF(data, cf, "[%" PRId64 "] status: %s",
+                stream->id, curlx_dyn_ptr(&ctx->scratch));
   }
   else {
-    CURL_TRC_CF(x->data, x->cf, "[%" PRIu64 "] header: %.*s: %.*s",
+    /* store as an HTTP1-style header */
+    CURL_TRC_CF(data, cf, "[%" PRId64 "] header: %.*s: %.*s",
                 stream->id, (int)name_len, name,
                 (int)value_len, value);
-    result = write_resp_raw(x->cf, x->data, name, name_len);
+    curlx_dyn_reset(&ctx->scratch);
+    result = curlx_dyn_addn(&ctx->scratch,
+                            (const char *)name, name_len);
     if(!result)
-      result = write_resp_raw(x->cf, x->data, ": ", 2);
+      result = curlx_dyn_addn(&ctx->scratch, STRCONST(": "));
     if(!result)
-      result = write_resp_raw(x->cf, x->data, value, value_len);
+      result = curlx_dyn_addn(&ctx->scratch,
+                              (const char *)value, value_len);
     if(!result)
-      result = write_resp_raw(x->cf, x->data, "\r\n", 2);
+      result = curlx_dyn_addn(&ctx->scratch, STRCONST("\r\n"));
+    if(!result)
+      cf_quiche_write_hd(cf, data, stream, curlx_dyn_ptr(&ctx->scratch),
+                         curlx_dyn_len(&ctx->scratch), FALSE);
   }
+
   if(result) {
     CURL_TRC_CF(x->data, x->cf, "[%" PRIu64 "] on header error %d",
                 stream->id, result);
+    if(!stream->xfer_result)
+      stream->xfer_result = result;
   }
-  return result;
+  return stream->xfer_result ? 1 : 0;
 }
 
 static CURLcode stream_resp_read(void *reader_ctx,
@@ -410,94 +432,99 @@ static CURLcode stream_resp_read(void *reader_ctx,
   return CURLE_OK;
 }
 
-static CURLcode cf_recv_body(struct Curl_cfilter *cf,
-                             struct Curl_easy *data)
+static void cf_quiche_flush_body(struct Curl_cfilter *cf,
+                                 struct Curl_easy *data,
+                                 struct h3_stream_ctx *stream)
+{
+  struct cf_quiche_ctx *ctx = cf->ctx;
+  const uint8_t *buf;
+  size_t blen;
+
+  while(stream && !stream->xfer_result) {
+    if(Curl_bufq_peek(&ctx->writebuf, &buf, &blen)) {
+      stream->xfer_result = Curl_xfer_write_resp(
+        data, (const char *)buf, blen, FALSE);
+      Curl_bufq_skip(&ctx->writebuf, blen);
+      if(stream->xfer_result) {
+        CURL_TRC_CF(data, cf, "[%" PRId64 "] error %d writing %zu bytes"
+                    " of data", stream->id, stream->xfer_result, blen);
+      }
+    }
+    else
+      break;
+  }
+  Curl_bufq_reset(&ctx->writebuf);
+}
+
+static void cf_quiche_recv_body(struct Curl_cfilter *cf,
+                                struct Curl_easy *data,
+                                struct h3_stream_ctx *stream)
 {
   struct cf_quiche_ctx *ctx = cf->ctx;
-  struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, data);
   size_t nread;
   struct cb_ctx cb_ctx;
   CURLcode result = CURLE_OK;
 
   if(!stream)
-    return CURLE_RECV_ERROR;
-
-  if(!stream->resp_hds_complete) {
-    result = write_resp_raw(cf, data, "\r\n", 2);
-    if(result)
-      return result;
-    stream->resp_hds_complete = TRUE;
-  }
+    return;
 
+  /* Even when the transfer has already errored, we need to receive
+   * the data from quiche, as quiche will otherwise get stuck and
+   * raise events to receive over and over again. */
   cb_ctx.cf = cf;
   cb_ctx.data = data;
-  result = Curl_bufq_slurp(&stream->recvbuf,
-                           stream_resp_read, &cb_ctx, &nread);
-
-  if(result && result != CURLE_AGAIN) {
-    CURL_TRC_CF(data, cf, "[%" PRIu64 "] recv_body error %zu",
-                stream->id, nread);
-    failf(data, "Error %d in HTTP/3 response body for stream[%" PRIu64 "]",
-          result, stream->id);
-    stream->closed = TRUE;
-    stream->reset = TRUE;
-    stream->send_closed = TRUE;
-    return result;
-  }
-  return CURLE_OK;
-}
-
-#ifdef DEBUGBUILD
-static const char *cf_ev_name(quiche_h3_event *ev)
-{
-  switch(quiche_h3_event_type(ev)) {
-  case QUICHE_H3_EVENT_HEADERS:
-    return "HEADERS";
-  case QUICHE_H3_EVENT_DATA:
-    return "DATA";
-  case QUICHE_H3_EVENT_RESET:
-    return "RESET";
-  case QUICHE_H3_EVENT_FINISHED:
-    return "FINISHED";
-  case QUICHE_H3_EVENT_GOAWAY:
-    return "GOAWAY";
-  default:
-    return "Unknown";
+  cb_ctx.stream = stream;
+  Curl_bufq_reset(&ctx->writebuf);
+  while(!result) {
+    result = Curl_bufq_slurp(&ctx->writebuf,
+                             stream_resp_read, &cb_ctx, &nread);
+    if(!result)
+      cf_quiche_flush_body(cf, data, stream);
+    else if(result == CURLE_AGAIN)
+      break;
+    else if(result) {
+      CURL_TRC_CF(data, cf, "[%" PRIu64 "] recv_body error %d",
+                  stream->id, result);
+      failf(data, "[%" PRIu64 "] Error %d in HTTP/3 response body for stream",
+            stream->id, result);
+      stream->closed = TRUE;
+      stream->reset = TRUE;
+      stream->send_closed = TRUE;
+      if(!stream->xfer_result)
+        stream->xfer_result = result;
+    }
   }
+  cf_quiche_flush_body(cf, data, stream);
 }
-#else
-#define cf_ev_name(x)   ""
-#endif
 
-static CURLcode h3_process_event(struct Curl_cfilter *cf,
+static void cf_quiche_process_ev(struct Curl_cfilter *cf,
                                  struct Curl_easy *data,
                                  struct h3_stream_ctx *stream,
                                  quiche_h3_event *ev)
 {
-  struct cb_ctx cb_ctx;
-  CURLcode result = CURLE_OK;
-  int rc;
-
   if(!stream)
-    return CURLE_OK;
+    return;
+
   switch(quiche_h3_event_type(ev)) {
-  case QUICHE_H3_EVENT_HEADERS:
+  case QUICHE_H3_EVENT_HEADERS: {
+    struct cb_ctx cb_ctx;
     stream->resp_got_header = TRUE;
     cb_ctx.cf = cf;
     cb_ctx.data = data;
-    rc = quiche_h3_event_for_each_header(ev, cb_each_header, &cb_ctx);
-    if(rc) {
-      failf(data, "Error %d in HTTP/3 response header for stream[%" PRIu64 "]",
-            rc, stream->id);
-      return CURLE_RECV_ERROR;
-    }
+    cb_ctx.stream = stream;
+    quiche_h3_event_for_each_header(ev, cb_each_header, &cb_ctx);
     CURL_TRC_CF(data, cf, "[%" PRIu64 "] <- [HEADERS]", stream->id);
+    Curl_multi_mark_dirty(data);
     break;
-
+  }
   case QUICHE_H3_EVENT_DATA:
-    if(!stream->closed) {
-      result = cf_recv_body(cf, data);
+    if(!stream->resp_hds_complete) {
+      stream->resp_hds_complete = TRUE;
+      cf_quiche_write_hd(cf, data, stream, "\r\n", 2, FALSE);
     }
+    cf_quiche_recv_body(cf, data, stream);
+    CURL_TRC_CF(data, cf, "[%" PRIu64 "] <- [DATA]", stream->id);
+    Curl_multi_mark_dirty(data);
     break;
 
   case QUICHE_H3_EVENT_RESET:
@@ -505,17 +532,17 @@ static CURLcode h3_process_event(struct Curl_cfilter *cf,
     stream->closed = TRUE;
     stream->reset = TRUE;
     stream->send_closed = TRUE;
+    Curl_multi_mark_dirty(data);
     break;
 
   case QUICHE_H3_EVENT_FINISHED:
     CURL_TRC_CF(data, cf, "[%" PRIu64 "] CLOSED", stream->id);
     if(!stream->resp_hds_complete) {
-      result = write_resp_raw(cf, data, "\r\n", 2);
-      if(result)
-        return result;
       stream->resp_hds_complete = TRUE;
+      cf_quiche_write_hd(cf, data, stream, "\r\n", 2, TRUE);
     }
     stream->closed = TRUE;
+    Curl_multi_mark_dirty(data);
     break;
 
   case QUICHE_H3_EVENT_GOAWAY:
@@ -527,20 +554,6 @@ static CURLcode h3_process_event(struct Curl_cfilter *cf,
                 stream->id, quiche_h3_event_type(ev));
     break;
   }
-  return result;
-}
-
-static CURLcode cf_quiche_ev_process(struct Curl_cfilter *cf,
-                                     struct Curl_easy *data,
-                                     struct h3_stream_ctx *stream,
-                                     quiche_h3_event *ev)
-{
-  CURLcode result = h3_process_event(cf, data, stream, ev);
-  Curl_multi_mark_dirty(data);
-  if(result)
-    CURL_TRC_CF(data, cf, "error processing event %s for [%" PRIu64 "] -> %d",
-                cf_ev_name(ev), stream->id, result);
-  return result;
 }
 
 struct cf_quich_disp_ctx {
@@ -548,7 +561,6 @@ struct cf_quich_disp_ctx {
   struct Curl_cfilter *cf;
   struct Curl_multi *multi;
   quiche_h3_event *ev;
-  CURLcode result;
 };
 
 static bool cf_quiche_disp_event(uint32_t mid, void *val, void *user_data)
@@ -559,7 +571,7 @@ static bool cf_quiche_disp_event(uint32_t mid, void *val, void *user_data)
   if(stream->id == dctx->stream_id) {
     struct Curl_easy *sdata = Curl_multi_get_easy(dctx->multi, mid);
     if(sdata)
-      dctx->result = cf_quiche_ev_process(dctx->cf, sdata, stream, dctx->ev);
+      cf_quiche_process_ev(dctx->cf, sdata, stream, dctx->ev);
     return FALSE; /* stop iterating */
   }
   return TRUE;
@@ -583,23 +595,22 @@ static CURLcode cf_poll_events(struct Curl_cfilter *cf,
       return CURLE_HTTP3;
     }
     else {
-      struct cf_quich_disp_ctx dctx;
-      dctx.stream_id = (uint64_t)rv;
-      dctx.cf = cf;
-      dctx.multi = data->multi;
-      dctx.ev = ev;
-      dctx.result = CURLE_OK;
       stream = H3_STREAM_CTX(ctx, data);
-      if(stream && stream->id == dctx.stream_id) {
+      if(stream && stream->id == (uint64_t)rv) {
         /* event for calling transfer */
-        CURLcode result = cf_quiche_ev_process(cf, data, stream, ev);
+        cf_quiche_process_ev(cf, data, stream, ev);
         quiche_h3_event_free(ev);
-        if(result)
-          return result;
+        if(stream->xfer_result)
+          return stream->xfer_result;
       }
       else {
         /* another transfer, do not return errors, as they are not for
          * the calling transfer */
+        struct cf_quich_disp_ctx dctx;
+        dctx.stream_id = (uint64_t)rv;
+        dctx.cf = cf;
+        dctx.multi = data->multi;
+        dctx.ev = ev;
         Curl_uint32_hash_visit(&ctx->streams, cf_quiche_disp_event, &dctx);
         quiche_h3_event_free(ev);
       }
@@ -844,63 +855,46 @@ static CURLcode recv_closed_stream(struct Curl_cfilter *cf,
 }
 
 static CURLcode cf_quiche_recv(struct Curl_cfilter *cf, struct Curl_easy *data,
-                               char *buf, size_t len, size_t *pnread)
+                               char *buf, size_t blen, size_t *pnread)
 {
   struct cf_quiche_ctx *ctx = cf->ctx;
   struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, data);
   CURLcode result = CURLE_OK;
 
   *pnread = 0;
+  (void)buf;
+  (void)blen;
   vquic_ctx_update_time(&ctx->q);
 
   if(!stream)
     return CURLE_RECV_ERROR;
 
-  if(!Curl_bufq_is_empty(&stream->recvbuf)) {
-    result = Curl_bufq_cread(&stream->recvbuf, buf, len, pnread);
-    CURL_TRC_CF(data, cf, "[%" PRIu64 "] read recvbuf(len=%zu) "
-                "-> %d, %zu", stream->id, len, result, *pnread);
-    if(result)
-      goto out;
-  }
-
   result = cf_process_ingress(cf, data);
   if(result) {
     CURL_TRC_CF(data, cf, "cf_recv, error on ingress");
     goto out;
   }
 
-  /* recvbuf had nothing before, maybe after progressing ingress? */
-  if(!*pnread && !Curl_bufq_is_empty(&stream->recvbuf)) {
-    result = Curl_bufq_cread(&stream->recvbuf, buf, len, pnread);
-    CURL_TRC_CF(data, cf, "[%" PRIu64 "] read recvbuf(len=%zu) "
-                "-> %d, %zu", stream->id, len, result, *pnread);
-    if(result)
-      goto out;
-  }
-
-  if(*pnread) {
-    if(stream->closed)
-      Curl_multi_mark_dirty(data);
+  if(stream->xfer_result) {
+    cf_quiche_stream_close(cf, data, stream);
+    result = stream->xfer_result;
+    goto out;
   }
-  else {
-    if(stream->closed)
-      result = recv_closed_stream(cf, data, pnread);
-    else if(quiche_conn_is_draining(ctx->qconn)) {
-      failf(data, "QUIC connection is draining");
-      result = CURLE_HTTP3;
-    }
-    else
-      result = CURLE_AGAIN;
+  else if(stream->closed)
+    result = recv_closed_stream(cf, data, pnread);
+  else if(quiche_conn_is_draining(ctx->qconn)) {
+    failf(data, "QUIC connection is draining");
+    result = CURLE_HTTP3;
   }
+  else
+    result = CURLE_AGAIN;
 
 out:
   result = Curl_1st_err(result, cf_flush_egress(cf, data));
   if(*pnread > 0)
     ctx->data_recvd += *pnread;
-  CURL_TRC_CF(data, cf, "[%" PRIu64 "] cf_recv(total=%"
-              FMT_OFF_T ") -> %d, %zu",
-              stream->id, ctx->data_recvd, result, *pnread);
+  CURL_TRC_CF(data, cf, "[%" PRIu64 "] cf_recv(len=%zu) -> %d, %zu, total=%"
+              FMT_OFF_T, stream->id, blen, result, *pnread, ctx->data_recvd);
   return result;
 }
 
@@ -1092,6 +1086,10 @@ static CURLcode cf_quiche_send(struct Curl_cfilter *cf, struct Curl_easy *data,
       goto out;
     stream = H3_STREAM_CTX(ctx, data);
   }
+  else if(stream->xfer_result) {
+    cf_quiche_stream_close(cf, data, stream);
+    result = stream->xfer_result;
+  }
   else if(stream->closed) {
     if(stream->resp_hds_complete) {
       /* sending request body on a stream that has been closed by the
@@ -1165,19 +1163,6 @@ static CURLcode cf_quiche_adjust_pollset(struct Curl_cfilter *cf,
   return result;
 }
 
-/*
- * Called from transfer.c:data_pending to know if we should keep looping
- * to receive more data from the connection.
- */
-static bool cf_quiche_data_pending(struct Curl_cfilter *cf,
-                                   const struct Curl_easy *data)
-{
-  struct cf_quiche_ctx *ctx = cf->ctx;
-  const struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, data);
-  (void)cf;
-  return stream && !Curl_bufq_is_empty(&stream->recvbuf);
-}
-
 static CURLcode h3_data_pause(struct Curl_cfilter *cf,
                               struct Curl_easy *data,
                               bool pause)
@@ -1608,7 +1593,7 @@ struct Curl_cftype Curl_cft_http3 = {
   cf_quiche_close,
   cf_quiche_shutdown,
   cf_quiche_adjust_pollset,
-  cf_quiche_data_pending,
+  Curl_cf_def_data_pending,
   cf_quiche_send,
   cf_quiche_recv,
   cf_quiche_cntrl,