 +    fin = r->header_only
 +          || (r->headers_out.content_length_n == 0 && !r->expect_trailers);
 +
-+    if (quiche_h3_send_response(h3c->h3, c->quic->conn, r->qstream->id,
-+                                headers->elts, headers->nelts, fin)) {
-+        ngx_array_destroy(headers);
++    rc = quiche_h3_send_response(h3c->h3, c->quic->conn, r->qstream->id,
++                                headers->elts, headers->nelts, fin);
++
++    ngx_array_destroy(headers);
++
++    if (rc == QUICHE_H3_ERR_STREAM_BLOCKED) {
++        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
++                       "http3 stream blocked %ui", r->qstream->id);
++
++        r->qstream->blocked = 1;
++
++        fc->write->active = 1;
++        fc->write->ready = 0;
++
++        return NGX_AGAIN;
++    }
++
++    if (rc != NGX_OK) {
 +        return NGX_ERROR;
 +    }
 +
++    r->qstream->headers_sent = 1;
++
 +    ngx_post_event(c->write, &ngx_posted_events);
-+
-+    ngx_array_destroy(headers);
 +
 +    return NGX_OK;
 +}

     if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
diff --git a/src/http/v3/ngx_http_v3.c b/src/http/v3/ngx_http_v3.c
new file mode 100644
index 000000000..38683d901
--- /dev/null
+++ b/src/http/v3/ngx_http_v3.c
@@ -0,0 +1,2099 @@
+
+/*
+ * Copyright (C) Cloudflare, Inc.

+}
diff --git a/src/http/v3/ngx_http_v3.h b/src/http/v3/ngx_http_v3.h
new file mode 100644
index 000000000..45a55b898
--- /dev/null
+++ b/src/http/v3/ngx_http_v3.h
@@ -0,0 +1,77 @@
+
+/*
+ * Copyright (C) Cloudflare, Inc.
