Skip to content

Fix H3 quiche traffic handling#13213

Open
bneradt wants to merge 1 commit into
apache:masterfrom
bneradt:h3-quiche-boringssl-test-fixes
Open

Fix H3 quiche traffic handling#13213
bneradt wants to merge 1 commit into
apache:masterfrom
bneradt:h3-quiche-boringssl-test-fixes

Conversation

@bneradt
Copy link
Copy Markdown
Contributor

@bneradt bneradt commented May 29, 2026

Overview

This patch extends the HTTP/3 autest coverage, using curl, golang, and
Proxy Verifier HTTP/3 clients to generate their implementations of h3
traffic. It also adds request and response bodies of various sizes,
including "large" 300k bodies to exercise multiple packet, buffer, and
flow control ATS HTTP/3 implementations. It also exercises some
interesting requests, such as HEAD and 204 responses. This patch also
includes the various production fixes needed for these tests.

Issues Found and their Fixes

UDP batches could stall large H3 transfers

Large request and response bodies exposed a UDP receive starvation bug
in the UDP read path. On systems using recvmmsg() with
edge-triggered readiness, ATS could read one full batch of datagrams
and then leave the rest queued in the kernel without another readable
event to wake the QUIC stack.

This changes
UDPNetProcessorInternal::read_multiple_messages_from_net() in
src/iocore/net/UnixUDPNet.cc to return whether the kernel supplied a
full batch. udp_read_from_net() now loops while full batches are
returned, draining the socket before handing packets to QUIC and
avoiding large-transfer stalls caused by unread UDP bursts.

QUIC stream writes consumed data before quiche accepted it

The stream write path consumed the QUICStreamVCAdapter write reader
inside _read(), before QUICStream::send_data() knew whether
quiche_conn_stream_send() had accepted the bytes. When quiche accepted
only a partial write or returned a flow-control error, ATS could lose
stream data and report write progress too early.

This makes QUICStream::send_data() keep a pending
IOBufferBlock/FIN pair until quiche reports successful consumption,
and only then calls the new QUICStreamAdapter::consume() hook. The
concrete reader accounting lives in QUICStreamVCAdapter::_consume(),
while QUICStream::has_data_to_send(), QUICStream::on_write(), and
QUICNetVConnection::on_stream_updated() make newly writable stream
data schedule packet writes again. This also treats completed finite
writes with only FIN left as writable stream state, so empty bodies and
fully consumed bodies still close the H3 stream cleanly.

H3 transaction cleanup raced with stream closure

The timeout and stream lifetime tests exposed cases where an
HQTransaction could be deleted while an event handler was still
active, or while the QUIC stream adapter still had read/write cleanup to
finish. That left later stream-close and timeout paths touching state
that had already been torn down.

This adds explicit transaction lifetime state in HQTransaction:
_closed, _stream_closed, _event_handler_active, and
_is_write_buffer_flushed(). Http3App::on_stream_close() now calls
HQTransaction::stream_closed(), and
HQTransaction::_delete_if_possible() waits until the transaction is
done, the stream is closed or no longer readable, and pending writes
have flushed before deleting the transaction.

H3 read completion could run before headers and DATA were settled

The H3 request read path could signal completion before asynchronous
QPACK header decode and buffered DATA delivery had finished updating the
sink VIO. That showed up around HEAD, 204, and stream-close timing
because the HTTP state machine needed a stable view of whether headers
were decoded and whether a request body existed.

This updates Http3HeaderVIOAdaptor::_on_qpack_decode_complete() to add
the printed header length to the sink VIO and notify
Http3Transaction::on_header_decode_complete(), which schedules the
appropriate read event. Http3StreamDataVIOAdaptor::finalize() now uses
a persistent reader, writes buffered DATA into the sink VIO exactly
once, and updates ndone/nbytes consistently before the transaction
is signaled.

The QPACK static table had drifted from the standard table

The HEAD, 204, and quic-go coverage exposed that ATS's static QPACK
table was not the table used by external HTTP/3 implementations. The
extra zstd entry and modified accept-encoding value in
src/proxy/http3/QPACK.cc shifted later static indexes, so an
externally encoded :status 204 could decode as a different status.

This restores the standard static table entries by using
accept-encoding: gzip, deflate, br and removing the non-standard
content-encoding: zstd entry. The new 204 cases in
tests/gold_tests/h3/replays/h3_proxy_verifier.replay.yaml and
tests/gold_tests/h3/replays/h3_server_for_go_client.replay.yaml cover
this interoperability point with Proxy Verifier and quic-go.

Copilot AI review requested due to automatic review settings May 29, 2026 04:04
@bneradt bneradt added this to the 11.0.0 milestone May 29, 2026
@bneradt bneradt self-assigned this May 29, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves HTTP/3 over QUIC handling for larger bodies and stream lifecycle edge cases, and adds H3 interoperability/timeout coverage around curl, Proxy Verifier, SNI, session tickets, and stream cleanup.

Changes:

  • Keeps QUIC stream write data pending until accepted, schedules packet writes on stream updates, and adjusts H3 transaction cleanup.
  • Drains UDP recvmmsg batches for edge-triggered sockets.
  • Adds/updates HTTP/3 gold tests and replay support for QUIC ports.

Reviewed changes

Copilot reviewed 32 out of 32 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/iocore/net/quic/QUICStream.cc Adds pending send-block handling and delayed consumption.
src/iocore/net/quic/QUICStreamVCAdapter.cc Defers write consumption and schedules stream write updates.
src/iocore/net/quic/QUICStreamAdapter.cc Adds explicit consume path.
src/iocore/net/QUICNetVConnection.cc Schedules packet writes when streams update.
src/iocore/net/UnixUDPNet.cc Drains full recvmmsg batches.
src/proxy/http3/Http3Transaction.cc Updates H3 transaction read/write/close lifecycle.
src/proxy/http3/Http3StreamDataVIOAdaptor.cc Buffers and finalizes DATA delivery via persistent reader.
src/proxy/http3/Http3HeaderVIOAdaptor.cc Tracks decoded header bytes and signals decode completion.
src/proxy/http3/Http3App.cc Notifies transactions when streams close.
include/** QUIC/H3 headers Adds APIs and state needed by the implementation changes.
include/iocore/net/quic/Mock.h Updates QUIC mocks for new interfaces.
src/iocore/net/P_UDPNet.h Updates UDP batch-read return type.
src/iocore/net/P_QUICNetVConnection.h Declares stream update callback.
tests/gold_tests/autest-site/ats_replay.test.ext Adds HTTP/3 port wiring for replay tests.
tests/gold_tests/h3/* Adds H3 curl, Proxy Verifier, stream lifetime, timeout, SNI, and session ticket tests/replays.
tests/gold_tests/timeout/* Updates QUIC feature gating for timeout tests.

Comment thread src/iocore/net/quic/QUICStream.cc
Comment thread tests/gold_tests/h3/h3_proxy_verifier.test.py
Comment thread tests/gold_tests/h3/h3_stream_lifetime.test.py
Comment thread tests/gold_tests/h3/h3_active_timeout.test.py
Comment thread src/iocore/net/quic/QUICStreamVCAdapter.cc Outdated
@zwoop
Copy link
Copy Markdown
Contributor

zwoop commented May 29, 2026

[approve ci autest 2]

@bneradt bneradt force-pushed the h3-quiche-boringssl-test-fixes branch from 778e8bb to f69c806 Compare May 29, 2026 20:10
Comment thread tests/gold_tests/h3/replays/h3_active_timeout.replay.yaml
Comment thread tests/gold_tests/h3/h3_active_timeout.test.py
@bneradt bneradt changed the title Fix H3 quiche large body handling Fix H3 quiche traffic handling May 29, 2026
@bneradt bneradt force-pushed the h3-quiche-boringssl-test-fixes branch from f69c806 to ba62f1b Compare May 29, 2026 22:52
Copilot AI review requested due to automatic review settings May 29, 2026 22:52
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 40 changed files in this pull request and generated 2 comments.

Comment thread src/iocore/net/quic/QUICStreamVCAdapter.cc Outdated
Comment thread tests/gold_tests/h3/h3_session_ticket.test.py
# Overview
This patch extends the HTTP/3 autest coverage, using curl, golang, and
Proxy Verifier HTTP/3 clients to generate their implementations of h3
traffic. It also adds request and response bodies of various sizes,
including "large" 300k bodies to exercise multiple packet, buffer, and
flow control ATS HTTP/3 implementations. It also exercises some
interesting requests, such as HEAD and 204 responses. This patch also
includes the various production fixes needed for these tests.

# Issues Found and their Fixes

## UDP batches could stall large H3 transfers
Large request and response bodies exposed a UDP receive starvation bug
in the UDP read path. On systems using `recvmmsg()` with
edge-triggered readiness, ATS could read one full batch of datagrams
and then leave the rest queued in the kernel without another readable
event to wake the QUIC stack.

This changes
`UDPNetProcessorInternal::read_multiple_messages_from_net()` in
`src/iocore/net/UnixUDPNet.cc` to return whether the kernel supplied a
full batch. `udp_read_from_net()` now loops while full batches are
returned, draining the socket before handing packets to QUIC and
avoiding large-transfer stalls caused by unread UDP bursts.

## QUIC stream writes consumed data before quiche accepted it
The stream write path consumed the `QUICStreamVCAdapter` write reader
inside `_read()`, before `QUICStream::send_data()` knew whether
`quiche_conn_stream_send()` had accepted the bytes. When quiche accepted
only a partial write or returned a flow-control error, ATS could lose
stream data and report write progress too early.

This makes `QUICStream::send_data()` keep a pending
`IOBufferBlock`/FIN pair until quiche reports successful consumption,
and only then calls the new `QUICStreamAdapter::consume()` hook. The
concrete reader accounting lives in `QUICStreamVCAdapter::_consume()`,
while `QUICStream::has_data_to_send()`, `QUICStream::on_write()`, and
`QUICNetVConnection::on_stream_updated()` make newly writable stream
data schedule packet writes again. This also treats completed finite
writes with only FIN left as writable stream state, so empty bodies and
fully consumed bodies still close the H3 stream cleanly.

## H3 transaction cleanup raced with stream closure
The timeout and stream lifetime tests exposed cases where an
`HQTransaction` could be deleted while an event handler was still
active, or while the QUIC stream adapter still had read/write cleanup to
finish. That left later stream-close and timeout paths touching state
that had already been torn down.

This adds explicit transaction lifetime state in `HQTransaction`:
`_closed`, `_stream_closed`, `_event_handler_active`, and
`_is_write_buffer_flushed()`. `Http3App::on_stream_close()` now calls
`HQTransaction::stream_closed()`, and
`HQTransaction::_delete_if_possible()` waits until the transaction is
done, the stream is closed or no longer readable, and pending writes
have flushed before deleting the transaction.

## H3 read completion could run before headers and DATA were settled
The H3 request read path could signal completion before asynchronous
QPACK header decode and buffered DATA delivery had finished updating the
sink VIO. That showed up around HEAD, 204, and stream-close timing
because the HTTP state machine needed a stable view of whether headers
were decoded and whether a request body existed.

This updates `Http3HeaderVIOAdaptor::_on_qpack_decode_complete()` to add
the printed header length to the sink VIO and notify
`Http3Transaction::on_header_decode_complete()`, which schedules the
appropriate read event. `Http3StreamDataVIOAdaptor::finalize()` now uses
a persistent reader, writes buffered DATA into the sink VIO exactly
once, and updates `ndone`/`nbytes` consistently before the transaction
is signaled.

## The QPACK static table had drifted from the standard table
The HEAD, 204, and quic-go coverage exposed that ATS's static QPACK
table was not the table used by external HTTP/3 implementations. The
extra zstd entry and modified `accept-encoding` value in
`src/proxy/http3/QPACK.cc` shifted later static indexes, so an
externally encoded `:status 204` could decode as a different status.

This restores the standard static table entries by using
`accept-encoding: gzip, deflate, br` and removing the non-standard
`content-encoding: zstd` entry. The new 204 cases in
`tests/gold_tests/h3/replays/h3_proxy_verifier.replay.yaml` and
`tests/gold_tests/h3/replays/h3_server_for_go_client.replay.yaml` cover
this interoperability point with Proxy Verifier and quic-go.
@bneradt bneradt force-pushed the h3-quiche-boringssl-test-fixes branch from ba62f1b to 6297520 Compare May 29, 2026 23:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants