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 7bbdc3370..d8cb2c12a 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 @@ -27,6 +27,7 @@ import com.vitorpamplona.quic.http3.Http3Frame import com.vitorpamplona.quic.http3.Http3FrameReader import com.vitorpamplona.quic.http3.Http3FrameType import com.vitorpamplona.quic.http3.Http3Settings +import com.vitorpamplona.quic.http3.Http3SettingsId import com.vitorpamplona.quic.http3.Http3StreamType import com.vitorpamplona.quic.qpack.QpackDecoder import com.vitorpamplona.quic.qpack.QpackEncoder @@ -62,12 +63,27 @@ class Http3GetClient( private val conn: QuicConnection, ) : GetClient { suspend fun init(scope: CoroutineScope) { - // Control stream: type-0x00 prefix followed by a SETTINGS frame - // (empty body is legal โ€” RFC 9114 ยง7.2.4). + // Control stream: type-0x00 prefix followed by a SETTINGS frame. + // Explicitly advertise QPACK_MAX_TABLE_CAPACITY=0 and + // QPACK_BLOCKED_STREAMS=0 โ€” our QpackDecoder is literal-only + // (no dynamic-table support, RFC 9204 Required Insert Count = 0). + // Empty SETTINGS would let the spec default kick in (also 0), + // but aioquic's behavior on multiplexing showed that without + // explicit signal it still emits dynamic-table-referenced fields + // in response HEADERS, which our decoder silently mis-parses + // (status=0, file not written). Sending the explicit zeros + // forces aioquic onto the literal-only QPACK path. val control = conn.openUniStream() val w = QuicWriter() w.writeVarint(Http3StreamType.CONTROL) - w.writeBytes(Http3Settings(emptyMap()).encodeFrame()) + w.writeBytes( + Http3Settings( + mapOf( + Http3SettingsId.QPACK_MAX_TABLE_CAPACITY to 0L, + Http3SettingsId.QPACK_BLOCKED_STREAMS to 0L, + ), + ).encodeFrame(), + ) control.send.enqueue(w.toByteArray()) // Control stream stays open for the lifetime of the H3 connection; // do NOT call finish() โ€” peers treat that as H3_CLOSED_CRITICAL_STREAM.