From c0d7b6031afb08a3acbd72eb32098d2cb4db479f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 00:48:28 +0000 Subject: [PATCH] fix(quic): restore PTO CRYPTO retransmit lost in qlog merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Direct evidence from aioquic interop qlog: packet_sent PN=0 frames=[crypto] (ClientHello) packet_sent PN=1 frames=[ping] (PTO probe — bare PING) packet_received frames=[connection_close] reason="Packet contains no CRYPTO frame" aioquic strictly rejects pre-handshake Initials that contain no CRYPTO frame. Our PTO probe was a bare PING, not a CRYPTO retransmit. Agent 2's c9e036f72 implemented the right behavior: on PTO, the driver requeues all inflight CRYPTO at the highest pre-application level, so the next drain emits a fresh CRYPTO frame at the original offset. The pendingPing flag stays as a fallback only used when no CRYPTO is available to retransmit. That logic was wiped during the qlog observer merge — driver's PTO branch only set pendingPing, nothing requeued anything, probe was always bare PING. Restored. Should fix the post-flush-fix regression where aioquic went 1/7 (only transfer green) — once a packet was dropped or PTO fired, the next probe was bare PING, aioquic closed with "no CRYPTO frame", the test failed. Sequential tests on the same matrix invocation likely intermittently passed/failed depending on whether PTO fired before the response arrived. The :quic helpers requeueAllInflightCrypto + highestPreApplicationLevel already exist on the branch — just the driver call wasn't wired. https://claude.ai/code/session_01HcvfQq1ttPV9PkRoJb4nyT --- .../quic/connection/QuicConnectionDriver.kt | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/quic/src/commonMain/kotlin/com/vitorpamplona/quic/connection/QuicConnectionDriver.kt b/quic/src/commonMain/kotlin/com/vitorpamplona/quic/connection/QuicConnectionDriver.kt index 7b4e5f037..b9081d927 100644 --- a/quic/src/commonMain/kotlin/com/vitorpamplona/quic/connection/QuicConnectionDriver.kt +++ b/quic/src/commonMain/kotlin/com/vitorpamplona/quic/connection/QuicConnectionDriver.kt @@ -145,14 +145,44 @@ class QuicConnectionDriver( Unit } if (woke == null) { - // PTO fired. Set pendingPing so the writer emits a - // PING on the next drain (RFC 9002 §6.2.4 probe - // packet). The peer's ACK feeds loss detection + - // retransmit (steps 5–6). + // PTO fired. RFC 9002 §6.2.4: the probe packet MUST + // be ack-eliciting at the encryption level with + // unacknowledged data, and SHOULD retransmit lost + // data rather than just emit a PING. + // + // Pre-1-RTT we have a concrete thing to retransmit: + // the unacknowledged ClientHello (at Initial) or + // ClientFinished (at Handshake). We requeue ALL + // currently-inflight CRYPTO bytes for the highest + // active pre-application level so the next drain's + // takeChunk emits a fresh CRYPTO frame at the + // original offset. The pendingPing flag stays set + // as a fallback — `collectHandshakeLevelFrames` + // skips the PING when a CRYPTO retransmit lands in + // the same frame list, so we don't waste a frame. + // + // Why this matters in interop: aioquic strictly + // rejects pre-handshake Initials that contain no + // CRYPTO frame (CONNECTION_CLOSE 0x0 "Packet + // contains no CRYPTO frame"). A bare-PING probe + // hits that immediately. With CRYPTO retransmit on + // PTO, the probe carries a re-emitted ClientHello + // and the server processes it normally. + // + // Post-handshake (1-RTT installed) we retain the + // bare-PING behavior; STREAM retransmit is driven + // by packet-number-threshold loss detection from + // the ACK that the PING elicits. val newPtoCount = connection.lock .withLock { connection.pendingPing = true + if (connection.application.sendProtection == null) { + val level = highestPreApplicationLevel(connection) + if (level != null) { + connection.requeueAllInflightCrypto(level) + } + } connection.consecutivePtoCount = (connection.consecutivePtoCount + 1).coerceAtMost(6) connection.consecutivePtoCount