diff --git a/quic/interop/src/main/kotlin/com/vitorpamplona/quic/interop/runner/Http3GetClient.kt b/quic/interop/src/main/kotlin/com/vitorpamplona/quic/interop/runner/Http3GetClient.kt index 40a03f212..7bbdc3370 100644 --- a/quic/interop/src/main/kotlin/com/vitorpamplona/quic/interop/runner/Http3GetClient.kt +++ b/quic/interop/src/main/kotlin/com/vitorpamplona/quic/interop/runner/Http3GetClient.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quic.interop.runner import com.vitorpamplona.quic.QuicWriter import com.vitorpamplona.quic.connection.QuicConnection +import com.vitorpamplona.quic.connection.drainPeerInitiatedUniStreamsIntoBlackHole import com.vitorpamplona.quic.http3.Http3Frame import com.vitorpamplona.quic.http3.Http3FrameReader import com.vitorpamplona.quic.http3.Http3FrameType @@ -29,6 +30,7 @@ import com.vitorpamplona.quic.http3.Http3Settings import com.vitorpamplona.quic.http3.Http3StreamType import com.vitorpamplona.quic.qpack.QpackDecoder import com.vitorpamplona.quic.qpack.QpackEncoder +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect /** Common shape for the two interop GET clients (HTTP/3 and HQ-interop). */ @@ -59,7 +61,7 @@ data class GetResponse( class Http3GetClient( private val conn: QuicConnection, ) : GetClient { - suspend fun init() { + suspend fun init(scope: CoroutineScope) { // Control stream: type-0x00 prefix followed by a SETTINGS frame // (empty body is legal — RFC 9114 §7.2.4). val control = conn.openUniStream() @@ -81,6 +83,16 @@ class Http3GetClient( val w3 = QuicWriter() w3.writeVarint(Http3StreamType.QPACK_DECODER) qpackDec.send.enqueue(w3.toByteArray()) + + // RFC 9114 §6.2: the server opens its own three uni streams + // (control, qpack encoder, qpack decoder). Their bytes accumulate + // in per-stream `incomingChannel`s (capacity 64); without an + // active consumer the channel saturates and `:quic` tears down + // the connection with INTERNAL_ERROR. We don't actually use the + // dynamic table or care about the server's settings, so drain + // and discard. Without this, multiplexing testcase fails after + // ~4.5s with "consumer overflowed" tear-down. + conn.drainPeerInitiatedUniStreamsIntoBlackHole(scope) } /** diff --git a/quic/interop/src/main/kotlin/com/vitorpamplona/quic/interop/runner/InteropClient.kt b/quic/interop/src/main/kotlin/com/vitorpamplona/quic/interop/runner/InteropClient.kt index ad21a95a7..111b7dc93 100644 --- a/quic/interop/src/main/kotlin/com/vitorpamplona/quic/interop/runner/InteropClient.kt +++ b/quic/interop/src/main/kotlin/com/vitorpamplona/quic/interop/runner/InteropClient.kt @@ -89,6 +89,16 @@ fun main() { else -> null } + // For the versionnegotiation testcase the runner expects us to send + // an Initial advertising a version the server doesn't support, then + // honor its VN response by retrying with v1. agent A's + // QuicVersion.FORCE_VERSION_NEGOTIATION drives that flow. + val initialVersion = + when (testcase) { + "versionnegotiation" -> com.vitorpamplona.quic.packet.QuicVersion.FORCE_VERSION_NEGOTIATION + else -> com.vitorpamplona.quic.packet.QuicVersion.V1 + } + // ALPN selection. Different servers configure different ALPNs PER // testcase, with no consistent convention: // - quic-go-qns — strictly hq-interop for non-http3 tests @@ -131,13 +141,14 @@ fun main() { "handshake", "chacha20", "handshakeloss", "transfer", "http3", "multiplexing", "transferloss", "transfercorruption", "longrtt", "goodput", "crosstraffic", - "retry", "ipv6", + "retry", "ipv6", "versionnegotiation", -> { runTransferTest( requests = requests, downloadsDir = downloadsDir, cipherSuites = cipherSuites, offeredAlpns = offeredAlpns, + initialVersion = initialVersion, keyLogPath = keyLogPath, parallel = (testcase == "multiplexing"), ) @@ -168,6 +179,7 @@ private fun runTransferTest( downloadsDir: File, cipherSuites: IntArray?, offeredAlpns: List, + initialVersion: Int, keyLogPath: String?, parallel: Boolean, ): Int { @@ -204,6 +216,7 @@ private fun runTransferTest( config = QuicConnectionConfig(), tlsCertificateValidator = PermissiveCertificateValidator(), alpnList = offeredAlpns.map { it.wireBytes }, + initialVersion = initialVersion, cipherSuites = cipherSuites ?: intArrayOf( @@ -235,7 +248,7 @@ private fun runTransferTest( val client: GetClient = when (negotiated) { "h3" -> { - Http3GetClient(conn).also { it.init() } + Http3GetClient(conn).also { it.init(scope) } } "hq-interop" -> {