From 0a7d7fbb8ce441d5dfccebe3f743ed817fe354c8 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Fri, 17 Jul 2026 12:19:39 -0400 Subject: [PATCH 1/3] Add ECH service metadata to Route. Also combine DnsOverHttpsServer with FakeDns, so we can serve metadata anywhere we're testing DNS. Also move stuff to the 'internal' package in okhttp-dnsoverhttps, so we can make symbols visible to the main okhttp module's tests. --- .../kotlin/okhttp.jvm-conventions.gradle.kts | 1 + .../okhttp.publish-conventions.gradle.kts | 5 +- .../okhttp3/dnsoverhttps/DnsOverHttps.kt | 9 + .../{ => internal}/-DnsMessage.kt | 24 ++- .../{ => internal}/-DnsMessageReader.kt | 6 +- .../{ => internal}/-DnsMessageWriter.kt | 6 +- .../{ => internal}/-DnsOverHttpsCall.kt | 4 +- .../dnsoverhttps/DnsOverHttpsServer.kt | 141 -------------- .../okhttp3/dnsoverhttps/DnsOverHttpsTest.kt | 83 ++++---- .../dnsoverhttps/DnsRecordCodecTest.kt | 7 + .../DnsMessageReaderRecordedValuesTest.kt | 2 +- .../DnsMessageReaderWriterTest.kt | 2 +- okhttp-testing-support/build.gradle.kts | 1 + .../src/main/kotlin/okhttp3/FakeDns.kt | 177 +++++++++++++++--- okhttp/api/android/okhttp.api | 2 + okhttp/api/jvm/okhttp.api | 2 + .../commonJvmAndroid/kotlin/okhttp3/Route.kt | 16 +- .../connection/ForceConnectRoutePlanner.kt | 25 --- .../internal/connection/InetAddressOrder.kt | 6 +- .../internal/connection/RouteSelector.kt | 113 ++++++----- .../okhttp3/internal/dns/-LookupDnsCall.kt | 1 - .../okhttp3/ConnectionCoalescingTest.kt | 2 +- .../kotlin/okhttp3/internal/TestAndSetTest.kt | 2 - .../connection/InetAddressOrderTest.kt | 39 ++-- .../internal/connection/RouteSelectorTest.kt | 9 + 25 files changed, 375 insertions(+), 310 deletions(-) rename okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/{ => internal}/-DnsMessage.kt (94%) rename okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/{ => internal}/-DnsMessageReader.kt (98%) rename okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/{ => internal}/-DnsMessageWriter.kt (98%) rename okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/{ => internal}/-DnsOverHttpsCall.kt (98%) delete mode 100644 okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsServer.kt rename okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/{ => internal}/DnsMessageReaderRecordedValuesTest.kt (99%) rename okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/{ => internal}/DnsMessageReaderWriterTest.kt (99%) delete mode 100644 okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ForceConnectRoutePlanner.kt diff --git a/build-logic/src/main/kotlin/okhttp.jvm-conventions.gradle.kts b/build-logic/src/main/kotlin/okhttp.jvm-conventions.gradle.kts index 6138b8f833fd..f479330692c5 100644 --- a/build-logic/src/main/kotlin/okhttp.jvm-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/okhttp.jvm-conventions.gradle.kts @@ -44,6 +44,7 @@ tasks.withType { "-Xjvm-default=all", "-Xexpect-actual-classes", ) + optIn.add("okhttp3.internal.OkHttpInternalApi") } } diff --git a/build-logic/src/main/kotlin/okhttp.publish-conventions.gradle.kts b/build-logic/src/main/kotlin/okhttp.publish-conventions.gradle.kts index 26d19a05a4d8..b23f1adaf5be 100644 --- a/build-logic/src/main/kotlin/okhttp.publish-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/okhttp.publish-conventions.gradle.kts @@ -64,11 +64,12 @@ configure { } configure { - ignoredPackages += "okhttp3.logging.internal" ignoredPackages += "mockwebserver3.internal" - ignoredPackages += "okhttp3.internal" ignoredPackages += "mockwebserver3.junit5.internal" ignoredPackages += "okhttp3.brotli.internal" + ignoredPackages += "okhttp3.dnsoverhttps.internal" + ignoredPackages += "okhttp3.internal" + ignoredPackages += "okhttp3.logging.internal" ignoredPackages += "okhttp3.sse.internal" ignoredPackages += "okhttp3.tls.internal" } diff --git a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt index 0343df75c25e..c10011a30424 100644 --- a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt +++ b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt @@ -28,6 +28,15 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response +import okhttp3.dnsoverhttps.internal.DnsMessage +import okhttp3.dnsoverhttps.internal.DnsOverHttpsCall +import okhttp3.dnsoverhttps.internal.QueryRequestBody +import okhttp3.dnsoverhttps.internal.ResourceRecord +import okhttp3.dnsoverhttps.internal.TYPE_A +import okhttp3.dnsoverhttps.internal.TYPE_AAAA +import okhttp3.dnsoverhttps.internal.TYPE_HTTPS +import okhttp3.dnsoverhttps.internal.asQueryParameter +import okhttp3.dnsoverhttps.internal.decodeResponse import okhttp3.internal.publicsuffix.PublicSuffixDatabase /** diff --git a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessage.kt b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/internal/-DnsMessage.kt similarity index 94% rename from okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessage.kt rename to okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/internal/-DnsMessage.kt index b318d450afaa..5b5625c116ae 100644 --- a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessage.kt +++ b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/internal/-DnsMessage.kt @@ -15,7 +15,7 @@ */ @file:Suppress("ktlint:standard:filename") -package okhttp3.dnsoverhttps +package okhttp3.dnsoverhttps.internal import java.io.IOException import java.net.InetAddress @@ -26,12 +26,14 @@ import okhttp3.RequestBody import okhttp3.Response import okhttp3.dnsoverhttps.DnsOverHttps.Companion.DNS_MESSAGE import okhttp3.dnsoverhttps.DnsOverHttps.Companion.MAX_RESPONSE_SIZE +import okhttp3.internal.OkHttpInternalApi import okhttp3.internal.platform.Platform import okio.Buffer import okio.BufferedSink import okio.ByteString -internal data class DnsMessage( +@OkHttpInternalApi +data class DnsMessage( val id: Short, val flags: Int, val questions: List, @@ -79,7 +81,7 @@ internal data class DnsMessage( } } -internal data class Question( +data class Question( val name: String, val type: Int, val `class`: Int = CLASS_IN, @@ -94,7 +96,7 @@ internal data class Question( } } -internal sealed interface ResourceRecord { +sealed interface ResourceRecord { val name: String val timeToLive: Int @@ -140,12 +142,18 @@ internal sealed interface ResourceRecord { } /** https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4 */ -internal const val TYPE_A = 1 -internal const val TYPE_AAAA = 28 -internal const val TYPE_HTTPS = 65 +@OkHttpInternalApi +const val TYPE_A = 1 + +@OkHttpInternalApi +const val TYPE_AAAA = 28 + +@OkHttpInternalApi +const val TYPE_HTTPS = 65 /** https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-2 */ -internal const val CLASS_IN = 1 +@OkHttpInternalApi +const val CLASS_IN = 1 /** https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-6 */ internal const val RESPONSE_CODE_SUCCESS = 0 diff --git a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/internal/-DnsMessageReader.kt similarity index 98% rename from okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt rename to okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/internal/-DnsMessageReader.kt index e93b2a4f06a3..7e833e35502f 100644 --- a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt +++ b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/internal/-DnsMessageReader.kt @@ -15,10 +15,11 @@ */ @file:Suppress("ktlint:standard:filename") -package okhttp3.dnsoverhttps +package okhttp3.dnsoverhttps.internal import java.net.InetAddress import okhttp3.Protocol +import okhttp3.internal.OkHttpInternalApi import okio.Buffer import okio.BufferedSource import okio.ByteString @@ -33,7 +34,8 @@ import okio.buffer * * https://datatracker.ietf.org/doc/html/rfc1035 */ -internal class DnsMessageReader( +@OkHttpInternalApi +class DnsMessageReader( source: BufferedSource, ) { /** diff --git a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageWriter.kt b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/internal/-DnsMessageWriter.kt similarity index 98% rename from okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageWriter.kt rename to okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/internal/-DnsMessageWriter.kt index 3dc248f7bf5a..19893684100f 100644 --- a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageWriter.kt +++ b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/internal/-DnsMessageWriter.kt @@ -15,11 +15,12 @@ */ @file:Suppress("ktlint:standard:filename") -package okhttp3.dnsoverhttps +package okhttp3.dnsoverhttps.internal import java.net.Inet4Address import java.net.Inet6Address import okhttp3.Protocol +import okhttp3.internal.OkHttpInternalApi import okio.Buffer import okio.ByteString import okio.ByteString.Companion.encodeUtf8 @@ -30,7 +31,8 @@ import okio.utf8Size * * https://datatracker.ietf.org/doc/html/rfc1035 */ -internal class DnsMessageWriter( +@OkHttpInternalApi +class DnsMessageWriter( private val sink: Buffer, ) { /** diff --git a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsOverHttpsCall.kt b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/internal/-DnsOverHttpsCall.kt similarity index 98% rename from okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsOverHttpsCall.kt rename to okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/internal/-DnsOverHttpsCall.kt index ade8c55da7e2..76ca09005d29 100644 --- a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsOverHttpsCall.kt +++ b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/internal/-DnsOverHttpsCall.kt @@ -14,9 +14,8 @@ * limitations under the License. */ @file:Suppress("ktlint:standard:filename") -@file:OptIn(OkHttpInternalApi::class) -package okhttp3.dnsoverhttps +package okhttp3.dnsoverhttps.internal import java.io.IOException import java.util.concurrent.atomic.AtomicReference @@ -35,6 +34,7 @@ import okhttp3.internal.testAndSet /** * Implements [Dns.Call] by making multiple HTTPS calls. */ +@OkHttpInternalApi internal class DnsOverHttpsCall( override val request: Dns.Request, private val calls: List, diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsServer.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsServer.kt deleted file mode 100644 index cedb7a6d695f..000000000000 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsServer.kt +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (c) 2026 OkHttp Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package okhttp3.dnsoverhttps - -import java.net.Inet4Address -import java.net.Inet6Address -import java.net.InetAddress -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.LinkedBlockingDeque -import java.util.concurrent.atomic.AtomicInteger -import mockwebserver3.Dispatcher -import mockwebserver3.MockResponse -import mockwebserver3.RecordedRequest -import okhttp3.Headers -import okio.Buffer -import okio.ByteString.Companion.decodeBase64 - -/** - * Handles DNS calls using in-memory records. - */ -internal class DnsOverHttpsServer : Dispatcher() { - private val data = ConcurrentHashMap>() - - var extraHeaders: Headers = Headers.headersOf() - val requests = LinkedBlockingDeque>() - val nextSequenceIndex = AtomicInteger(0) - val sequenceIndexToOverride = ConcurrentHashMap() - - operator fun set( - hostname: String, - records: List, - ) { - data[hostname] = records - } - - @JvmName("set-inetAddresses") // Avoid raw types collision. - operator fun set( - hostname: String, - inetAddresses: List, - ) { - set( - hostname, - inetAddresses.map { inetAddress -> - ResourceRecord.IpAddress( - name = hostname, - timeToLive = 5, - address = inetAddress, - ) - }, - ) - } - - fun takeRequest(): Pair = requests.take() - - fun pollRequest(): Pair? = requests.poll() - - override fun dispatch(request: RecordedRequest): MockResponse { - val sequenceIndex = nextSequenceIndex.getAndIncrement() - val override = sequenceIndexToOverride.remove(sequenceIndex) - if (override != null) return override - - val requestBody = request.body - val queryParameter = request.url.queryParameter("dns") - val encodedDnsQuery = - when { - requestBody != null -> Buffer().write(requestBody) - queryParameter != null -> Buffer().write(queryParameter.decodeBase64()!!) - else -> return MockResponse(code = 400) - } - - val dnsRequest = DnsMessageReader(encodedDnsQuery).read() - requests.put(request to dnsRequest) - - val dnsResponse = invoke(dnsRequest) - - val body = Buffer() - DnsMessageWriter(body).write(dnsResponse) - - return MockResponse - .Builder() - .addHeader("content-type", "application/dns-message") - .apply { - for ((name, value) in extraHeaders) { - addHeader(name, value) - } - }.body(body) - .build() - } - - fun invoke(request: DnsMessage): DnsMessage { - val answers = - buildList { - for (question in request.questions) { - val records = data[question.name] ?: continue - for (record in records) { - if (!record.matches(question)) continue - add(record) - } - } - } - - // QR = 1 (Response) - // OPCODE = 0 (standard query) - // RD = 1 (Recursion Desired) - // RA = 1 (Recursion Available) - // RCODE = 0 (success) - // QR OPCODE AA TC RD RA Z RCODE - val flags = 0b1___0000__0__0__1__1_000__0000 - - return DnsMessage( - id = request.id, - flags = flags, - questions = request.questions, - answers = answers, - ) - } - - private fun ResourceRecord.matches(question: Question): Boolean { - if (question.`class` != CLASS_IN) return false - - return when (question.type) { - TYPE_A -> (this as? ResourceRecord.IpAddress)?.address is Inet4Address - TYPE_AAAA -> (this as? ResourceRecord.IpAddress)?.address is Inet6Address - TYPE_HTTPS -> this is ResourceRecord.Https - else -> false - } - } -} diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt index f5cd1f6001e5..f4d1567cd6af 100644 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt @@ -39,11 +39,18 @@ import okhttp3.CallEvent.CacheMiss import okhttp3.Dispatcher import okhttp3.Dns import okhttp3.EventRecorder +import okhttp3.FakeDns import okhttp3.Headers.Companion.headersOf import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Protocol import okhttp3.Response +import okhttp3.dnsoverhttps.internal.CLASS_IN +import okhttp3.dnsoverhttps.internal.DnsMessage +import okhttp3.dnsoverhttps.internal.Question +import okhttp3.dnsoverhttps.internal.ResourceRecord +import okhttp3.dnsoverhttps.internal.TYPE_A +import okhttp3.dnsoverhttps.internal.TYPE_AAAA import okhttp3.testing.PlatformRule import okio.Buffer import okio.ByteString.Companion.decodeHex @@ -72,13 +79,13 @@ class DnsOverHttpsTest( @RegisterExtension val platform = PlatformRule() - private val dnsOverHttpsServer = DnsOverHttpsServer() + private val server = FakeDns() @StartStop - private val server = + private val mockWebServer = MockWebServer() .apply { - dispatcher = dnsOverHttpsServer + dispatcher = server.dispatcher } private lateinit var dns: DnsOverHttps @@ -106,16 +113,16 @@ class DnsOverHttpsTest( @BeforeEach fun setUp() { - server.protocols = bootstrapClient.protocols + mockWebServer.protocols = bootstrapClient.protocols dns = buildLocalhost(bootstrapClient) } @Test fun getOne() { - dnsOverHttpsServer["lysine.dev"] = listOf(InetAddress.getByName("10.20.30.40")) + server["lysine.dev"] = listOf(InetAddress.getByName("10.20.30.40")) val result = dns.invoke(entryPoint, "lysine.dev") assertThat(result).isEqualTo(listOf(address("10.20.30.40"))) - val (httpsRequest, dnsRequest) = dnsOverHttpsServer.takeRequest() + val (httpsRequest, dnsRequest) = server.takeRequest() assertThat(httpsRequest.method).isEqualTo("GET") assertThat(dnsRequest) .isEqualTo(queryRequest("lysine.dev", TYPE_A)) @@ -123,7 +130,7 @@ class DnsOverHttpsTest( @Test fun getIpv6() { - dnsOverHttpsServer["lysine.dev"] = + server["lysine.dev"] = listOf( InetAddress.getByName("10.20.30.40"), InetAddress.getByName("1:2::3:4"), @@ -135,11 +142,11 @@ class DnsOverHttpsTest( address("10.20.30.40"), ) - val (httpsRequest1, dnsRequest1) = dnsOverHttpsServer.takeRequest() + val (httpsRequest1, dnsRequest1) = server.takeRequest() assertThat(httpsRequest1.method).isEqualTo("GET") assertThat(dnsRequest1).isEqualTo(queryRequest("lysine.dev", TYPE_AAAA)) - val (httpsRequest2, dnsRequest2) = dnsOverHttpsServer.takeRequest() + val (httpsRequest2, dnsRequest2) = server.takeRequest() assertThat(httpsRequest2.method).isEqualTo("GET") assertThat(dnsRequest2).isEqualTo(queryRequest("lysine.dev", TYPE_A)) } @@ -149,7 +156,7 @@ class DnsOverHttpsTest( assertFailsWith { dns(entryPoint, "lysine.dev") } - val (httpsRequest, dnsRequest) = dnsOverHttpsServer.takeRequest() + val (httpsRequest, dnsRequest) = server.takeRequest() assertThat(httpsRequest.method).isEqualTo("GET") assertThat(dnsRequest) .isEqualTo(queryRequest("lysine.dev", TYPE_A)) @@ -164,7 +171,7 @@ class DnsOverHttpsTest( dns(entryPoint, "dev") } assertThat(e).hasMessage("private hosts not resolved") - assertThat(dnsOverHttpsServer.pollRequest()).isNull() + assertThat(server.pollRequest()).isNull() } @Test @@ -176,13 +183,13 @@ class DnsOverHttpsTest( dns(entryPoint, "lysine.dev") } assertThat(e).hasMessage("public hosts not resolved") - assertThat(dnsOverHttpsServer.pollRequest()).isNull() + assertThat(server.pollRequest()).isNull() } @Test fun failOnExcessiveResponse() { val array = CharArray(128 * 1024 + 2) { '0' } - dnsOverHttpsServer.sequenceIndexToOverride[0] = overrideResponse(String(array)) + server.sequenceIndexToOverride[0] = overrideResponse(String(array)) val e = assertFailsWith { dns(entryPoint, "lysine.dev") @@ -193,7 +200,7 @@ class DnsOverHttpsTest( @Test fun failOnBadResponse() { - dnsOverHttpsServer.sequenceIndexToOverride[0] = overrideResponse("00") + server.sequenceIndexToOverride[0] = overrideResponse("00") val e = assertFailsWith { dns(entryPoint, "lysine.dev") @@ -214,17 +221,17 @@ class DnsOverHttpsTest( val cachedClient = bootstrapClient.newBuilder().cache(cache).build() val cachedDns = buildLocalhost(cachedClient) - dnsOverHttpsServer.extraHeaders = + server.extraHeaders = headersOf( "cache-control", "private, max-age=298", ) - dnsOverHttpsServer["lysine.dev"] = listOf(InetAddress.getByName("10.20.30.40")) - dnsOverHttpsServer["alternate.lysine.dev"] = listOf(InetAddress.getByName("55.66.77.88")) + server["lysine.dev"] = listOf(InetAddress.getByName("10.20.30.40")) + server["alternate.lysine.dev"] = listOf(InetAddress.getByName("55.66.77.88")) val result1 = cachedDns(entryPoint, "lysine.dev") assertThat(result1).containsExactly(address("10.20.30.40")) - val (httpsRequest1, dnsRequest1) = dnsOverHttpsServer.takeRequest() + val (httpsRequest1, dnsRequest1) = server.takeRequest() assertThat(httpsRequest1.method).isEqualTo("GET") assertThat(dnsRequest1) .isEqualTo(queryRequest("lysine.dev", TYPE_A)) @@ -232,14 +239,14 @@ class DnsOverHttpsTest( assertThat(cacheEvents()).containsExactly(CacheMiss::class) val result2 = cachedDns(entryPoint, "lysine.dev") - assertThat(dnsOverHttpsServer.pollRequest()).isNull() + assertThat(server.pollRequest()).isNull() assertThat(result2).isEqualTo(listOf(address("10.20.30.40"))) assertThat(cacheEvents()).containsExactly(CacheHit::class) val result3 = cachedDns(entryPoint, "alternate.lysine.dev") assertThat(result3).containsExactly(address("55.66.77.88")) - val (httpsRequest2, dnsRequest2) = dnsOverHttpsServer.takeRequest() + val (httpsRequest2, dnsRequest2) = server.takeRequest() assertThat(httpsRequest2.method).isEqualTo("GET") assertThat(dnsRequest2) .isEqualTo(queryRequest("alternate.lysine.dev", TYPE_A)) @@ -252,17 +259,17 @@ class DnsOverHttpsTest( val cache = Cache(cacheFs, "cache".toPath(), (100 * 1024).toLong()) val cachedClient = bootstrapClient.newBuilder().cache(cache).build() val cachedDns = buildLocalhost(cachedClient, post = true) - dnsOverHttpsServer.extraHeaders = + server.extraHeaders = headersOf( "cache-control", "private, max-age=298", ) - dnsOverHttpsServer["lysine.dev"] = listOf(InetAddress.getByName("10.20.30.40")) - dnsOverHttpsServer["alternate.lysine.dev"] = listOf(InetAddress.getByName("55.66.77.88")) + server["lysine.dev"] = listOf(InetAddress.getByName("10.20.30.40")) + server["alternate.lysine.dev"] = listOf(InetAddress.getByName("55.66.77.88")) val result1 = cachedDns(entryPoint, "lysine.dev") assertThat(result1).containsExactly(address("10.20.30.40")) - val (httpsRequest1, _) = dnsOverHttpsServer.takeRequest() + val (httpsRequest1, _) = server.takeRequest() assertThat(httpsRequest1.method).isEqualTo("POST") assertThat(httpsRequest1.url.encodedQuery) .isEqualTo("ct") @@ -270,14 +277,14 @@ class DnsOverHttpsTest( assertThat(cacheEvents()).containsExactly(CacheMiss::class) val result2 = cachedDns(entryPoint, "lysine.dev") - assertThat(dnsOverHttpsServer.pollRequest()).isNull() + assertThat(server.pollRequest()).isNull() assertThat(result2).isEqualTo(listOf(address("10.20.30.40"))) assertThat(cacheEvents()).containsExactly(CacheHit::class) val result3 = cachedDns(entryPoint, "alternate.lysine.dev") assertThat(result3).containsExactly(address("55.66.77.88")) - val (httpsRequest2, _) = dnsOverHttpsServer.takeRequest() + val (httpsRequest2, _) = server.takeRequest() assertThat(httpsRequest2.method).isEqualTo("POST") assertThat(httpsRequest2.url.encodedQuery) .isEqualTo("ct") @@ -290,15 +297,15 @@ class DnsOverHttpsTest( val cache = Cache(File("./target/DnsOverHttpsTest.cache"), 100 * 1024L) val cachedClient = bootstrapClient.newBuilder().cache(cache).build() val cachedDns = buildLocalhost(cachedClient) - dnsOverHttpsServer.extraHeaders = + server.extraHeaders = headersOf( "cache-control", "no-store", ) - dnsOverHttpsServer["lysine.dev"] = listOf(InetAddress.getByName("10.20.30.40")) + server["lysine.dev"] = listOf(InetAddress.getByName("10.20.30.40")) val result1 = cachedDns(entryPoint, "lysine.dev") assertThat(result1).containsExactly(address("10.20.30.40")) - val (httpsRequest1, dnsRequest1) = dnsOverHttpsServer.takeRequest() + val (httpsRequest1, dnsRequest1) = server.takeRequest() assertThat(httpsRequest1.method).isEqualTo("GET") assertThat(dnsRequest1) .isEqualTo(queryRequest("lysine.dev", TYPE_A)) @@ -307,7 +314,7 @@ class DnsOverHttpsTest( val result2 = cachedDns(entryPoint, "lysine.dev") assertThat(result2).isEqualTo(listOf(address("10.20.30.40"))) - val (httpsRequest2, dnsRequest2) = dnsOverHttpsServer.takeRequest() + val (httpsRequest2, dnsRequest2) = server.takeRequest() assertThat(httpsRequest2.method).isEqualTo("GET") assertThat(dnsRequest2) .isEqualTo(queryRequest("lysine.dev", TYPE_A)) @@ -320,7 +327,7 @@ class DnsOverHttpsTest( assumeTrue(entryPoint == EntryPoint.NewCall) dns = buildLocalhost(bootstrapClient, includeIPv6 = true, includeHttps = true) - dnsOverHttpsServer["lysine.dev"] = + server["lysine.dev"] = listOf( ResourceRecord.IpAddress( name = "lysine.dev", @@ -402,8 +409,8 @@ class DnsOverHttpsTest( dns = buildLocalhost(bootstrapClient, includeIPv6 = true, includeHttps = true) // Fail the HTTPS call, which should have index 0. - dnsOverHttpsServer.sequenceIndexToOverride[0] = overrideResponse("") - dnsOverHttpsServer["lysine.dev"] = + server.sequenceIndexToOverride[0] = overrideResponse("") + server["lysine.dev"] = listOf( ResourceRecord.IpAddress( name = "lysine.dev", @@ -455,8 +462,8 @@ class DnsOverHttpsTest( dns = buildLocalhost(bootstrapClient, includeIPv6 = true, includeHttps = true) // Fail the IPv6 call, which should have index 1. - dnsOverHttpsServer.sequenceIndexToOverride[1] = overrideResponse("") - dnsOverHttpsServer["lysine.dev"] = + server.sequenceIndexToOverride[1] = overrideResponse("") + server["lysine.dev"] = listOf( ResourceRecord.IpAddress( name = "lysine.dev", @@ -489,7 +496,7 @@ class DnsOverHttpsTest( assumeTrue(entryPoint == EntryPoint.NewCall) dns = buildLocalhost(bootstrapClient, includeIPv6 = true, includeHttps = true) - dnsOverHttpsServer["lysine.dev"] = + server["lysine.dev"] = listOf( ResourceRecord.IpAddress( name = "lysine.dev", @@ -569,7 +576,7 @@ class DnsOverHttpsTest( assumeTrue(entryPoint == EntryPoint.NewCall) dns = buildLocalhost(bootstrapClient, includeIPv6 = true) - dnsOverHttpsServer["lysine.dev"] = + server["lysine.dev"] = listOf( ResourceRecord.IpAddress( name = "lysine.dev", @@ -623,7 +630,7 @@ class DnsOverHttpsTest( resolvePrivateAddresses: Boolean = true, resolvePublicAddresses: Boolean = true, ): DnsOverHttps { - val url = server.url("/lookup?ct") + val url = mockWebServer.url("/lookup?ct") return DnsOverHttps .Builder() .client(bootstrapClient) diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsRecordCodecTest.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsRecordCodecTest.kt index 0a9c70651b7b..0554f7a82122 100644 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsRecordCodecTest.kt +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsRecordCodecTest.kt @@ -22,6 +22,13 @@ import assertk.assertions.isEqualTo import java.net.InetAddress import java.net.UnknownHostException import kotlin.test.assertFailsWith +import okhttp3.dnsoverhttps.internal.DnsMessage +import okhttp3.dnsoverhttps.internal.DnsMessageReader +import okhttp3.dnsoverhttps.internal.RESPONSE_CODE_SUCCESS +import okhttp3.dnsoverhttps.internal.ResourceRecord +import okhttp3.dnsoverhttps.internal.TYPE_A +import okhttp3.dnsoverhttps.internal.TYPE_AAAA +import okhttp3.dnsoverhttps.internal.asQueryParameter import okio.Buffer import okio.ByteString import okio.ByteString.Companion.decodeHex diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsMessageReaderRecordedValuesTest.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/internal/DnsMessageReaderRecordedValuesTest.kt similarity index 99% rename from okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsMessageReaderRecordedValuesTest.kt rename to okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/internal/DnsMessageReaderRecordedValuesTest.kt index 6e5687f220ef..1d17f575aba3 100644 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsMessageReaderRecordedValuesTest.kt +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/internal/DnsMessageReaderRecordedValuesTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package okhttp3.dnsoverhttps +package okhttp3.dnsoverhttps.internal import assertk.assertThat import assertk.assertions.containsExactly diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsMessageReaderWriterTest.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/internal/DnsMessageReaderWriterTest.kt similarity index 99% rename from okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsMessageReaderWriterTest.kt rename to okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/internal/DnsMessageReaderWriterTest.kt index de80efeddcea..b428943fffa1 100644 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsMessageReaderWriterTest.kt +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/internal/DnsMessageReaderWriterTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package okhttp3.dnsoverhttps +package okhttp3.dnsoverhttps.internal import assertk.assertThat import assertk.assertions.isEqualTo diff --git a/okhttp-testing-support/build.gradle.kts b/okhttp-testing-support/build.gradle.kts index 690b5f25b4c4..c132d91e946b 100644 --- a/okhttp-testing-support/build.gradle.kts +++ b/okhttp-testing-support/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { api(libs.square.okio) api(projects.mockwebserver3) "friendsApi"(projects.okhttp) + "friendsApi"(projects.okhttpDnsoverhttps) api(projects.okhttpTls) api(libs.assertk) api(libs.bouncycastle.bcprov) diff --git a/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt b/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt index 8c7691450de5..924b392862e7 100644 --- a/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt +++ b/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012 Square, Inc. + * Copyright (c) 2026 OkHttp Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,47 +17,143 @@ package okhttp3 import assertk.assertThat import assertk.assertions.containsExactly +import java.net.Inet4Address +import java.net.Inet6Address import java.net.InetAddress import java.net.UnknownHostException -import java.util.Collections +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.LinkedBlockingDeque +import java.util.concurrent.atomic.AtomicInteger +import mockwebserver3.Dispatcher +import mockwebserver3.MockResponse +import mockwebserver3.RecordedRequest +import okhttp3.dnsoverhttps.internal.CLASS_IN +import okhttp3.dnsoverhttps.internal.DnsMessage +import okhttp3.dnsoverhttps.internal.DnsMessageReader +import okhttp3.dnsoverhttps.internal.DnsMessageWriter +import okhttp3.dnsoverhttps.internal.Question +import okhttp3.dnsoverhttps.internal.ResourceRecord +import okhttp3.dnsoverhttps.internal.TYPE_A +import okhttp3.dnsoverhttps.internal.TYPE_AAAA +import okhttp3.dnsoverhttps.internal.TYPE_HTTPS import okio.Buffer +import okio.ByteString.Companion.decodeBase64 +/** + * Handles DNS calls using in-memory records. + */ class FakeDns : Dns { - private val hostAddresses: MutableMap> = - Collections.synchronizedMap(mutableMapOf()) - private val requestedHosts: MutableList = Collections.synchronizedList(mutableListOf()) + private val data = ConcurrentHashMap>() + + var extraHeaders: Headers = Headers.headersOf() + val requests = LinkedBlockingDeque>() + val nextSequenceIndex = AtomicInteger(0) + val sequenceIndexToOverride = ConcurrentHashMap() private var nextAddress = 0xff000064L // 255.0.0.100 in IPv4; ::ff00:64 in IPv6. - /** Sets the results for `hostname`. */ + /** Returns a MockWebServer dispatcher that serves DNS over HTTPS records. */ + val dispatcher = + object : Dispatcher() { + override fun dispatch(request: RecordedRequest): MockResponse { + val sequenceIndex = nextSequenceIndex.getAndIncrement() + val override = sequenceIndexToOverride.remove(sequenceIndex) + if (override != null) return override + + val requestBody = request.body + val queryParameter = request.url.queryParameter("dns") + val encodedDnsQuery = + when { + requestBody != null -> Buffer().write(requestBody) + queryParameter != null -> Buffer().write(queryParameter.decodeBase64()!!) + else -> return MockResponse(code = 400) + } + + val dnsRequest = DnsMessageReader(encodedDnsQuery).read() + requests.put(request to dnsRequest) + + val dnsResponse = invoke(dnsRequest) + + val body = Buffer() + DnsMessageWriter(body).write(dnsResponse) + + return MockResponse + .Builder() + .addHeader("content-type", "application/dns-message") + .apply { + for ((name, value) in extraHeaders) { + addHeader(name, value) + } + }.body(body) + .build() + } + } + operator fun set( hostname: String, - addresses: List, - ): FakeDns { - hostAddresses[hostname] = addresses - return this + records: List, + ) { + data[hostname] = records } - /** Clears the results for `hostname`. */ - fun clear(hostname: String): FakeDns { - hostAddresses.remove(hostname) - return this + @JvmName("set-inetAddresses") // Avoid raw types collision. + operator fun set( + hostname: String, + inetAddresses: List, + ) { + set( + hostname, + inetAddresses.map { inetAddress -> + ResourceRecord.IpAddress( + name = hostname, + timeToLive = 5, + address = inetAddress, + ) + }, + ) + } + + @Throws(UnknownHostException::class) + override fun lookup(hostname: String): List { + val records = data[hostname] ?: throw UnknownHostException() + + return records + .filterIsInstance() + .map { it.address } } @Throws(UnknownHostException::class) fun lookup( hostname: String, index: Int, - ): InetAddress = hostAddresses[hostname]!![index] + ): InetAddress = lookup(hostname)[index] - @Throws(UnknownHostException::class) - override fun lookup(hostname: String): List { - requestedHosts.add(hostname) - return hostAddresses[hostname] ?: throw UnknownHostException() + /** Clears the results for `hostname`. */ + fun clear(hostname: String) { + data.remove(hostname) } + fun takeRequest(): Pair = requests.take() + + fun pollRequest(): Pair? = requests.poll() + + fun takeAllRequests(): List> = + buildList { + while (true) { + val pair = pollRequest() + add(pair ?: break) + } + } + + /** + * Takes all requests received thus far (for all question types), and asserts that they match + * [expectedHosts]. + */ fun assertRequests(vararg expectedHosts: String?) { - assertThat(requestedHosts).containsExactly(*expectedHosts) - requestedHosts.clear() + val actualHosts = + takeAllRequests() + .flatMap { (_, dnsMessage) -> dnsMessage.questions } + .map { it.name } + assertThat(actualHosts).containsExactly(*expectedHosts) } /** Allocates and returns `count` fake IPv4 addresses like [255.0.0.100, 255.0.0.101]. */ @@ -83,4 +179,43 @@ class FakeDns : Dns { ) } } + + fun invoke(request: DnsMessage): DnsMessage { + val answers = + buildList { + for (question in request.questions) { + val records = data[question.name] ?: continue + for (record in records) { + if (!record.matches(question)) continue + add(record) + } + } + } + + // QR = 1 (Response) + // OPCODE = 0 (standard query) + // RD = 1 (Recursion Desired) + // RA = 1 (Recursion Available) + // RCODE = 0 (success) + // QR OPCODE AA TC RD RA Z RCODE + val flags = 0b1___0000__0__0__1__1_000__0000 + + return DnsMessage( + id = request.id, + flags = flags, + questions = request.questions, + answers = answers, + ) + } + + private fun ResourceRecord.matches(question: Question): Boolean { + if (question.`class` != CLASS_IN) return false + + return when (question.type) { + TYPE_A -> (this as? ResourceRecord.IpAddress)?.address is Inet4Address + TYPE_AAAA -> (this as? ResourceRecord.IpAddress)?.address is Inet6Address + TYPE_HTTPS -> this is ResourceRecord.Https + else -> false + } + } } diff --git a/okhttp/api/android/okhttp.api b/okhttp/api/android/okhttp.api index 52d93bbab938..40236699c4f3 100644 --- a/okhttp/api/android/okhttp.api +++ b/okhttp/api/android/okhttp.api @@ -1303,7 +1303,9 @@ public final class okhttp3/Route { public final fun -deprecated_proxy ()Ljava/net/Proxy; public final fun -deprecated_socketAddress ()Ljava/net/InetSocketAddress; public fun (Lokhttp3/Address;Ljava/net/Proxy;Ljava/net/InetSocketAddress;)V + public fun (Lokhttp3/Address;Ljava/net/Proxy;Ljava/net/InetSocketAddress;Lokio/ByteString;)V public final fun address ()Lokhttp3/Address; + public final fun echConfigList ()Lokio/ByteString; public fun equals (Ljava/lang/Object;)Z public fun hashCode ()I public final fun proxy ()Ljava/net/Proxy; diff --git a/okhttp/api/jvm/okhttp.api b/okhttp/api/jvm/okhttp.api index c1d30a4a2725..c7f6ecfed025 100644 --- a/okhttp/api/jvm/okhttp.api +++ b/okhttp/api/jvm/okhttp.api @@ -1302,7 +1302,9 @@ public final class okhttp3/Route { public final fun -deprecated_proxy ()Ljava/net/Proxy; public final fun -deprecated_socketAddress ()Ljava/net/InetSocketAddress; public fun (Lokhttp3/Address;Ljava/net/Proxy;Ljava/net/InetSocketAddress;)V + public fun (Lokhttp3/Address;Ljava/net/Proxy;Ljava/net/InetSocketAddress;Lokio/ByteString;)V public final fun address ()Lokhttp3/Address; + public final fun echConfigList ()Lokio/ByteString; public fun equals (Ljava/lang/Object;)Z public fun hashCode ()I public final fun proxy ()Ljava/net/Proxy; diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Route.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Route.kt index 87d5fce4a069..1c086778dd73 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Route.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Route.kt @@ -18,6 +18,7 @@ package okhttp3 import java.net.InetSocketAddress import java.net.Proxy import okhttp3.internal.toCanonicalHost +import okio.ByteString /** * The concrete route used by a connection to reach an abstract origin server. When creating a @@ -40,7 +41,14 @@ class Route( */ @get:JvmName("proxy") val proxy: Proxy, @get:JvmName("socketAddress") val socketAddress: InetSocketAddress, + @get:JvmName("echConfigList") val echConfigList: ByteString?, ) { + constructor( + address: Address, + proxy: Proxy, + socketAddress: InetSocketAddress, + ) : this(address, proxy, socketAddress, null) + @JvmName("-deprecated_address") @Deprecated( message = "moved to val", @@ -81,13 +89,15 @@ class Route( other is Route && other.address == address && other.proxy == proxy && - other.socketAddress == socketAddress + other.socketAddress == socketAddress && + other.echConfigList == echConfigList override fun hashCode(): Int { var result = 17 result = 31 * result + address.hashCode() result = 31 * result + proxy.hashCode() result = 31 * result + socketAddress.hashCode() + result = 31 * result + echConfigList.hashCode() return result } @@ -127,5 +137,9 @@ class Route( append(":") append(socketAddress.port) } + + if (echConfigList != null) { + append(" with ECH") + } } } diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ForceConnectRoutePlanner.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ForceConnectRoutePlanner.kt deleted file mode 100644 index e8c0fd0873b2..000000000000 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ForceConnectRoutePlanner.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2024 Square, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package okhttp3.internal.connection - -/** - * A RoutePlanner that will always establish a new connection, ignoring any connection pooling - */ -class ForceConnectRoutePlanner( - private val delegate: RealRoutePlanner, -) : RoutePlanner by delegate { - override fun plan(): RoutePlanner.Plan = delegate.planConnect() // not delegate.plan() -} diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/InetAddressOrder.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/InetAddressOrder.kt index f8f5db3b52d5..9aaadb91b394 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/InetAddressOrder.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/InetAddressOrder.kt @@ -17,7 +17,7 @@ package okhttp3.internal.connection import java.net.Inet6Address -import java.net.InetAddress +import okhttp3.Route import okhttp3.internal.interleave /** @@ -30,12 +30,12 @@ import okhttp3.internal.interleave * * https://datatracker.ietf.org/doc/html/rfc8305#section-4 */ -internal fun reorderForHappyEyeballs(addresses: List): List { +internal fun reorderForHappyEyeballs(addresses: List): List { if (addresses.size < 2) { return addresses } - val (ipv6, ipv4) = addresses.partition { it is Inet6Address } + val (ipv6, ipv4) = addresses.partition { it.socketAddress.address is Inet6Address } return if (ipv6.isEmpty() || ipv4.isEmpty()) { addresses diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RouteSelector.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RouteSelector.kt index 548d2bac48b4..385a8dc7665d 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RouteSelector.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RouteSelector.kt @@ -42,9 +42,6 @@ class RouteSelector internal constructor( private var proxies = emptyList() private var nextProxyIndex: Int = 0 - // State for negotiating the next socket address to use. - private var inetSocketAddresses = emptyList() - // State for negotiating failed routes private val postponedRoutes = mutableListOf() @@ -67,9 +64,8 @@ class RouteSelector internal constructor( // Postponed routes are always tried last. For example, if we have 2 proxies and all the // routes for proxy1 should be postponed, we'll move to proxy2. Only after we've exhausted // all the good routes will we attempt the postponed routes. - val proxy = nextProxy() - for (inetSocketAddress in inetSocketAddresses) { - val route = Route(address, proxy, inetSocketAddress) + val proxyRoutes = nextProxy() + for (route in proxyRoutes) { if (routeDatabase.shouldPostpone(route)) { postponedRoutes += route } else { @@ -129,24 +125,19 @@ class RouteSelector internal constructor( /** Returns the next proxy to try. May be PROXY.NO_PROXY but never null. */ @Throws(IOException::class) - private fun nextProxy(): Proxy { + private fun nextProxy(): List { if (!hasNextProxy()) { throw SocketException( "No route to ${address.url.host}; exhausted proxy configurations: $proxies", ) } val result = proxies[nextProxyIndex++] - resetNextInetSocketAddress(result) - return result + return nextRoutes(result) } - /** Prepares the socket addresses to attempt for the current proxy or host. */ + /** Returns the routes to attempt for [proxy]. */ @Throws(IOException::class) - private fun resetNextInetSocketAddress(proxy: Proxy) { - // Clear the addresses. Necessary if getAllByName() below throws! - val mutableInetSocketAddresses = mutableListOf() - inetSocketAddresses = mutableInetSocketAddresses - + private fun nextRoutes(proxy: Proxy): List { val socketHost: String val socketPort: Int if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) { @@ -166,34 +157,59 @@ class RouteSelector internal constructor( } if (proxy.type() == Proxy.Type.SOCKS) { - mutableInetSocketAddresses += InetSocketAddress.createUnresolved(socketHost, socketPort) - } else { - val addresses = - if (socketHost.canParseAsIpAddress()) { - listOf(InetAddress.getByName(socketHost)) - } else { - call.eventListener.dnsStart(call, socketHost) + return listOf( + Route( + address, + proxy, + InetSocketAddress.createUnresolved(socketHost, socketPort), + ), + ) + } - val result = address.dns.lookup(socketHost) - if (result.isEmpty()) { - throw UnknownHostException("${address.dns} returned no addresses for $socketHost") - } + if (socketHost.canParseAsIpAddress()) { + return listOf( + Route( + address, + proxy, + InetSocketAddress(InetAddress.getByName(socketHost), socketPort), + ), + ) + } - call.eventListener.dnsEnd(call, socketHost, result) - result - } + val routes = dnsLookup(proxy, socketHost, socketPort) - // Try each address for best behavior in mixed IPv4/IPv6 environments. - val orderedAddresses = - when { - fastFallback -> reorderForHappyEyeballs(addresses) - else -> addresses - } + // Try each address for best behavior in mixed IPv4/IPv6 environments. + return when { + fastFallback -> reorderForHappyEyeballs(routes) + else -> routes + } + } - for (inetAddress in orderedAddresses) { - mutableInetSocketAddresses += InetSocketAddress(inetAddress, socketPort) - } + private fun dnsLookup( + proxy: Proxy, + socketHost: String, + socketPort: Int, + ): List { + call.eventListener.dnsStart(call, socketHost) + + // TODO: switch to newCall() API to get ECH. + // TODO: switch to newCall() API to make this async. + val inetAddresses = address.dns.lookup(socketHost) + if (inetAddresses.isEmpty()) { + throw UnknownHostException("${address.dns} returned no addresses for $socketHost") } + + val result = + inetAddresses.map { inetAddress -> + Route( + address, + proxy, + InetSocketAddress(inetAddress, socketPort), + ) + } + + call.eventListener.dnsEnd(call, socketHost, inetAddresses) + return result } /** A set of selected Routes. */ @@ -212,15 +228,16 @@ class RouteSelector internal constructor( companion object { /** Obtain a host string containing either an actual host name or a numeric IP address. */ - val InetSocketAddress.socketHost: String get() { - // The InetSocketAddress was specified with a string (either a numeric IP or a host name). If - // it is a name, all IPs for that name should be tried. If it is an IP address, only that IP - // address should be tried. - val address = address ?: return hostName - - // The InetSocketAddress has a specific address: we should only try that address. Therefore we - // return the address and ignore any host name that may be available. - return address.hostAddress - } + val InetSocketAddress.socketHost: String + get() { + // The InetSocketAddress was specified with a string (either a numeric IP or a host name). + // If it is a name, all IPs for that name should be tried. If it is an IP address, only that + // IP address should be tried. + val address = address ?: return hostName + + // The InetSocketAddress has a specific address: we should only try that address. Therefore, + // we return the address and ignore any host name that may be available. + return address.hostAddress + } } } diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-LookupDnsCall.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-LookupDnsCall.kt index 63bba954dd88..c78fbeca6025 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-LookupDnsCall.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-LookupDnsCall.kt @@ -14,7 +14,6 @@ * limitations under the License. */ @file:Suppress("ktlint:standard:filename") -@file:OptIn(OkHttpInternalApi::class) package okhttp3.internal.dns diff --git a/okhttp/src/jvmTest/kotlin/okhttp3/ConnectionCoalescingTest.kt b/okhttp/src/jvmTest/kotlin/okhttp3/ConnectionCoalescingTest.kt index f7333f6382c1..67587aeffc57 100644 --- a/okhttp/src/jvmTest/kotlin/okhttp3/ConnectionCoalescingTest.kt +++ b/okhttp/src/jvmTest/kotlin/okhttp3/ConnectionCoalescingTest.kt @@ -89,7 +89,7 @@ class ConnectionCoalescingTest { dns["san.com"] = serverIps dns["nonsan.com"] = serverIps dns["www.wildcard.com"] = serverIps - dns["differentdns.com"] = listOf() + dns["differentdns.com"] = listOf() val handshakeCertificates = HandshakeCertificates .Builder() diff --git a/okhttp/src/jvmTest/kotlin/okhttp3/internal/TestAndSetTest.kt b/okhttp/src/jvmTest/kotlin/okhttp3/internal/TestAndSetTest.kt index 0a82865436cd..e0b4628a8028 100644 --- a/okhttp/src/jvmTest/kotlin/okhttp3/internal/TestAndSetTest.kt +++ b/okhttp/src/jvmTest/kotlin/okhttp3/internal/TestAndSetTest.kt @@ -13,8 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@file:OptIn(OkHttpInternalApi::class) - package okhttp3.internal import assertk.assertThat diff --git a/okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/InetAddressOrderTest.kt b/okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/InetAddressOrderTest.kt index 2d07d92f834a..ab2dc0dff275 100644 --- a/okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/InetAddressOrderTest.kt +++ b/okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/InetAddressOrderTest.kt @@ -20,17 +20,24 @@ import assertk.assertThat import assertk.assertions.isEqualTo import java.net.Inet4Address import java.net.Inet6Address +import java.net.InetAddress +import java.net.InetSocketAddress +import okhttp3.Route +import okhttp3.TestValueFactory import org.junit.jupiter.api.Test @Suppress("ktlint:standard:property-naming") class InetAddressOrderTest { - val ipv4_10_0_0_6 = Inet4Address.getByName("10.0.0.6") - val ipv4_10_0_0_1 = Inet4Address.getByName("10.0.0.1") - val ipv4_10_0_0_4 = Inet4Address.getByName("10.0.0.4") - val ipv6_ab = Inet6Address.getByName("::ac") - val ipv6_fc = Inet6Address.getByName("::fc") + private val factory = TestValueFactory() - @Test fun prioritiseIpv6Example() { + private val ipv4_10_0_0_6 = route(Inet4Address.getByName("10.0.0.6")) + private val ipv4_10_0_0_1 = route(Inet4Address.getByName("10.0.0.1")) + private val ipv4_10_0_0_4 = route(Inet4Address.getByName("10.0.0.4")) + private val ipv6_ab = route(Inet6Address.getByName("::ac")) + private val ipv6_fc = route(Inet6Address.getByName("::fc")) + + @Test + fun prioritiseIpv6Example() { val result = reorderForHappyEyeballs( listOf( @@ -47,7 +54,8 @@ class InetAddressOrderTest { ) } - @Test fun ipv6Only() { + @Test + fun ipv6Only() { val result = reorderForHappyEyeballs(listOf(ipv6_ab, ipv6_fc)) assertThat(result).isEqualTo( @@ -55,7 +63,8 @@ class InetAddressOrderTest { ) } - @Test fun ipv4Only() { + @Test + fun ipv4Only() { val result = reorderForHappyEyeballs( listOf( @@ -70,7 +79,8 @@ class InetAddressOrderTest { ) } - @Test fun singleIpv6() { + @Test + fun singleIpv6() { val result = reorderForHappyEyeballs(listOf(ipv6_ab)) assertThat(result).isEqualTo( @@ -78,7 +88,8 @@ class InetAddressOrderTest { ) } - @Test fun singleIpv4() { + @Test + fun singleIpv4() { val result = reorderForHappyEyeballs(listOf(ipv4_10_0_0_6)) assertThat(result).isEqualTo( @@ -86,11 +97,17 @@ class InetAddressOrderTest { ) } - @Test fun prioritiseIpv6() { + @Test + fun prioritiseIpv6() { val result = reorderForHappyEyeballs(listOf(ipv4_10_0_0_6, ipv6_ab)) assertThat(result).isEqualTo( listOf(ipv6_ab, ipv4_10_0_0_6), ) } + + private fun route(inetAddress: InetAddress): Route = + factory.newRoute( + socketAddress = InetSocketAddress(inetAddress, 443), + ) } diff --git a/okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/RouteSelectorTest.kt b/okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/RouteSelectorTest.kt index faa5db3fdd30..44e0994309f4 100644 --- a/okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/RouteSelectorTest.kt +++ b/okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/RouteSelectorTest.kt @@ -39,6 +39,7 @@ import okhttp3.TestValueFactory import okhttp3.internal.connection.RouteSelector.Companion.socketHost import okhttp3.internal.http.RecordingProxySelector import okhttp3.testing.PlatformRule +import okio.ByteString.Companion.encodeUtf8 import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -522,6 +523,14 @@ class RouteSelectorTest { InetSocketAddress(ipv4Address, 5678), ).toString(), ).isEqualTo("example.com:1003 via proxy 1.2.3.4:5678") + assertThat( + Route( + factory.newAddress(uriHost = "1.2.3.4", uriPort = 1003), + Proxy.NO_PROXY, + InetSocketAddress(ipv4Address, 1003), + "I am an ECH config".encodeUtf8(), + ).toString(), + ).isEqualTo("1.2.3.4:1003 with ECH") } @Test fun routeToStringIpv6() { From 947c097288f8aadd19f0f263d8689bdee6f8e187 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Fri, 17 Jul 2026 13:54:49 -0400 Subject: [PATCH 2/3] Fixup tests --- .../okhttp3/dnsoverhttps/DnsOverHttpsTest.kt | 21 ++++---- .../src/main/kotlin/okhttp3/FakeDns.kt | 50 ++++++++++++------ .../internal/connection/RouteSelectorTest.kt | 52 +++++++++---------- 3 files changed, 70 insertions(+), 53 deletions(-) diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt index f4d1567cd6af..411cb445ee67 100644 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt @@ -40,6 +40,7 @@ import okhttp3.Dispatcher import okhttp3.Dns import okhttp3.EventRecorder import okhttp3.FakeDns +import okhttp3.FakeDns.Request.DnsOverHttpsRequest import okhttp3.Headers.Companion.headersOf import okhttp3.Interceptor import okhttp3.OkHttpClient @@ -122,7 +123,7 @@ class DnsOverHttpsTest( server["lysine.dev"] = listOf(InetAddress.getByName("10.20.30.40")) val result = dns.invoke(entryPoint, "lysine.dev") assertThat(result).isEqualTo(listOf(address("10.20.30.40"))) - val (httpsRequest, dnsRequest) = server.takeRequest() + val (httpsRequest, dnsRequest) = server.takeRequest() as DnsOverHttpsRequest assertThat(httpsRequest.method).isEqualTo("GET") assertThat(dnsRequest) .isEqualTo(queryRequest("lysine.dev", TYPE_A)) @@ -142,11 +143,11 @@ class DnsOverHttpsTest( address("10.20.30.40"), ) - val (httpsRequest1, dnsRequest1) = server.takeRequest() + val (httpsRequest1, dnsRequest1) = server.takeRequest() as DnsOverHttpsRequest assertThat(httpsRequest1.method).isEqualTo("GET") assertThat(dnsRequest1).isEqualTo(queryRequest("lysine.dev", TYPE_AAAA)) - val (httpsRequest2, dnsRequest2) = server.takeRequest() + val (httpsRequest2, dnsRequest2) = server.takeRequest() as DnsOverHttpsRequest assertThat(httpsRequest2.method).isEqualTo("GET") assertThat(dnsRequest2).isEqualTo(queryRequest("lysine.dev", TYPE_A)) } @@ -156,7 +157,7 @@ class DnsOverHttpsTest( assertFailsWith { dns(entryPoint, "lysine.dev") } - val (httpsRequest, dnsRequest) = server.takeRequest() + val (httpsRequest, dnsRequest) = server.takeRequest() as DnsOverHttpsRequest assertThat(httpsRequest.method).isEqualTo("GET") assertThat(dnsRequest) .isEqualTo(queryRequest("lysine.dev", TYPE_A)) @@ -231,7 +232,7 @@ class DnsOverHttpsTest( val result1 = cachedDns(entryPoint, "lysine.dev") assertThat(result1).containsExactly(address("10.20.30.40")) - val (httpsRequest1, dnsRequest1) = server.takeRequest() + val (httpsRequest1, dnsRequest1) = server.takeRequest() as DnsOverHttpsRequest assertThat(httpsRequest1.method).isEqualTo("GET") assertThat(dnsRequest1) .isEqualTo(queryRequest("lysine.dev", TYPE_A)) @@ -246,7 +247,7 @@ class DnsOverHttpsTest( val result3 = cachedDns(entryPoint, "alternate.lysine.dev") assertThat(result3).containsExactly(address("55.66.77.88")) - val (httpsRequest2, dnsRequest2) = server.takeRequest() + val (httpsRequest2, dnsRequest2) = server.takeRequest() as DnsOverHttpsRequest assertThat(httpsRequest2.method).isEqualTo("GET") assertThat(dnsRequest2) .isEqualTo(queryRequest("alternate.lysine.dev", TYPE_A)) @@ -269,7 +270,7 @@ class DnsOverHttpsTest( val result1 = cachedDns(entryPoint, "lysine.dev") assertThat(result1).containsExactly(address("10.20.30.40")) - val (httpsRequest1, _) = server.takeRequest() + val (httpsRequest1, _) = server.takeRequest() as DnsOverHttpsRequest assertThat(httpsRequest1.method).isEqualTo("POST") assertThat(httpsRequest1.url.encodedQuery) .isEqualTo("ct") @@ -284,7 +285,7 @@ class DnsOverHttpsTest( val result3 = cachedDns(entryPoint, "alternate.lysine.dev") assertThat(result3).containsExactly(address("55.66.77.88")) - val (httpsRequest2, _) = server.takeRequest() + val (httpsRequest2, _) = server.takeRequest() as DnsOverHttpsRequest assertThat(httpsRequest2.method).isEqualTo("POST") assertThat(httpsRequest2.url.encodedQuery) .isEqualTo("ct") @@ -305,7 +306,7 @@ class DnsOverHttpsTest( server["lysine.dev"] = listOf(InetAddress.getByName("10.20.30.40")) val result1 = cachedDns(entryPoint, "lysine.dev") assertThat(result1).containsExactly(address("10.20.30.40")) - val (httpsRequest1, dnsRequest1) = server.takeRequest() + val (httpsRequest1, dnsRequest1) = server.takeRequest() as DnsOverHttpsRequest assertThat(httpsRequest1.method).isEqualTo("GET") assertThat(dnsRequest1) .isEqualTo(queryRequest("lysine.dev", TYPE_A)) @@ -314,7 +315,7 @@ class DnsOverHttpsTest( val result2 = cachedDns(entryPoint, "lysine.dev") assertThat(result2).isEqualTo(listOf(address("10.20.30.40"))) - val (httpsRequest2, dnsRequest2) = server.takeRequest() + val (httpsRequest2, dnsRequest2) = server.takeRequest() as DnsOverHttpsRequest assertThat(httpsRequest2.method).isEqualTo("GET") assertThat(dnsRequest2) .isEqualTo(queryRequest("lysine.dev", TYPE_A)) diff --git a/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt b/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt index 924b392862e7..c93dd45bc819 100644 --- a/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt +++ b/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt @@ -46,7 +46,7 @@ class FakeDns : Dns { private val data = ConcurrentHashMap>() var extraHeaders: Headers = Headers.headersOf() - val requests = LinkedBlockingDeque>() + private val requests = LinkedBlockingDeque() val nextSequenceIndex = AtomicInteger(0) val sequenceIndexToOverride = ConcurrentHashMap() private var nextAddress = 0xff000064L // 255.0.0.100 in IPv4; ::ff00:64 in IPv6. @@ -69,7 +69,7 @@ class FakeDns : Dns { } val dnsRequest = DnsMessageReader(encodedDnsQuery).read() - requests.put(request to dnsRequest) + requests.put(Request.DnsOverHttpsRequest(request, dnsRequest)) val dnsResponse = invoke(dnsRequest) @@ -114,29 +114,32 @@ class FakeDns : Dns { @Throws(UnknownHostException::class) override fun lookup(hostname: String): List { - val records = data[hostname] ?: throw UnknownHostException() + requests.put(Request.FunctionCall(hostname)) + val result = get(hostname) + if (result.isEmpty()) throw UnknownHostException() + return result + } + + /** Note that this request is not recorded. */ + @Throws(UnknownHostException::class) + operator fun get(hostname: String): List { + val records = data[hostname] ?: return listOf() return records .filterIsInstance() .map { it.address } } - @Throws(UnknownHostException::class) - fun lookup( - hostname: String, - index: Int, - ): InetAddress = lookup(hostname)[index] - /** Clears the results for `hostname`. */ fun clear(hostname: String) { data.remove(hostname) } - fun takeRequest(): Pair = requests.take() + fun takeRequest(): Request = requests.take() - fun pollRequest(): Pair? = requests.poll() + fun pollRequest(): Request? = requests.poll() - fun takeAllRequests(): List> = + fun takeAllRequests(): List = buildList { while (true) { val pair = pollRequest() @@ -149,11 +152,8 @@ class FakeDns : Dns { * [expectedHosts]. */ fun assertRequests(vararg expectedHosts: String?) { - val actualHosts = - takeAllRequests() - .flatMap { (_, dnsMessage) -> dnsMessage.questions } - .map { it.name } - assertThat(actualHosts).containsExactly(*expectedHosts) + val actualHostnames = takeAllRequests().map { it.hostname } + assertThat(actualHostnames).containsExactly(*expectedHosts) } /** Allocates and returns `count` fake IPv4 addresses like [255.0.0.100, 255.0.0.101]. */ @@ -218,4 +218,20 @@ class FakeDns : Dns { else -> false } } + + sealed interface Request { + val hostname: String + + data class DnsOverHttpsRequest( + val httpRequest: RecordedRequest, + val dnsRequest: DnsMessage, + ) : Request { + override val hostname: String + get() = dnsRequest.questions.single().name + } + + data class FunctionCall( + override val hostname: String, + ) : Request + } } diff --git a/okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/RouteSelectorTest.kt b/okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/RouteSelectorTest.kt index 44e0994309f4..266c82c14721 100644 --- a/okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/RouteSelectorTest.kt +++ b/okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/RouteSelectorTest.kt @@ -87,7 +87,7 @@ class RouteSelectorTest { assertThat(routeSelector.hasNext()).isTrue() dns[uriHost] = dns.allocate(1) val selection = routeSelector.next() - assertRoute(selection.next(), address, Proxy.NO_PROXY, dns.lookup(uriHost, 0), uriPort) + assertRoute(selection.next(), address, Proxy.NO_PROXY, dns[uriHost][0], uriPort) dns.assertRequests(uriHost) assertThat(selection.hasNext()).isFalse() assertFailsWith { @@ -109,7 +109,7 @@ class RouteSelectorTest { routeDatabase.failed(route) routeSelector = newRouteSelector(address) selection = routeSelector.next() - assertRoute(selection.next(), address, Proxy.NO_PROXY, dns.lookup(uriHost, 0), uriPort) + assertRoute(selection.next(), address, Proxy.NO_PROXY, dns[uriHost][0], uriPort) assertThat(selection.hasNext()).isFalse() assertFailsWith { selection.next() @@ -129,8 +129,8 @@ class RouteSelectorTest { assertThat(routeSelector.hasNext()).isTrue() dns[PROXY_A_HOST] = dns.allocate(2) val selection = routeSelector.next() - assertRoute(selection.next(), address, proxyA, dns.lookup(PROXY_A_HOST, 0), PROXY_A_PORT) - assertRoute(selection.next(), address, proxyA, dns.lookup(PROXY_A_HOST, 1), PROXY_A_PORT) + assertRoute(selection.next(), address, proxyA, dns[PROXY_A_HOST][0], PROXY_A_PORT) + assertRoute(selection.next(), address, proxyA, dns[PROXY_A_HOST][1], PROXY_A_PORT) assertThat(selection.hasNext()).isFalse() assertThat(routeSelector.hasNext()).isFalse() dns.assertRequests(PROXY_A_HOST) @@ -146,8 +146,8 @@ class RouteSelectorTest { assertThat(routeSelector.hasNext()).isTrue() dns[uriHost] = dns.allocate(2) val selection = routeSelector.next() - assertRoute(selection.next(), address, Proxy.NO_PROXY, dns.lookup(uriHost, 0), uriPort) - assertRoute(selection.next(), address, Proxy.NO_PROXY, dns.lookup(uriHost, 1), uriPort) + assertRoute(selection.next(), address, Proxy.NO_PROXY, dns[uriHost][0], uriPort) + assertRoute(selection.next(), address, Proxy.NO_PROXY, dns[uriHost][1], uriPort) assertThat(selection.hasNext()).isFalse() assertThat(routeSelector.hasNext()).isFalse() dns.assertRequests(uriHost) @@ -171,7 +171,7 @@ class RouteSelectorTest { assertThat(routeSelector.hasNext()).isTrue() dns[bogusHostname] = dns.allocate(1) val selection = routeSelector.next() - assertRoute(selection.next(), address, Proxy.NO_PROXY, dns.lookup(bogusHostname, 0), uriPort) + assertRoute(selection.next(), address, Proxy.NO_PROXY, dns[bogusHostname][0], uriPort) assertThat(selection.hasNext()).isFalse() assertThat(routeSelector.hasNext()).isFalse() dns.assertRequests(bogusHostname) @@ -201,7 +201,7 @@ class RouteSelectorTest { assertThat(routeSelector.hasNext()).isTrue() dns[uriHost] = dns.allocate(1) val selection = routeSelector.next() - assertRoute(selection.next(), address, Proxy.NO_PROXY, dns.lookup(uriHost, 0), uriPort) + assertRoute(selection.next(), address, Proxy.NO_PROXY, dns[uriHost][0], uriPort) dns.assertRequests(uriHost) assertThat(selection.hasNext()).isFalse() assertThat(routeSelector.hasNext()).isFalse() @@ -230,7 +230,7 @@ class RouteSelectorTest { assertThat(routeSelector.hasNext()).isTrue() dns[uriHost] = dns.allocate(1) val selection = routeSelector.next() - assertRoute(selection.next(), address, Proxy.NO_PROXY, dns.lookup(uriHost, 0), uriPort) + assertRoute(selection.next(), address, Proxy.NO_PROXY, dns[uriHost][0], uriPort) dns.assertRequests(uriHost) assertThat(selection.hasNext()).isFalse() assertThat(routeSelector.hasNext()).isFalse() @@ -242,8 +242,8 @@ class RouteSelectorTest { assertThat(routeSelector.hasNext()).isTrue() dns[uriHost] = dns.allocate(2) val selection = routeSelector.next() - assertRoute(selection.next(), address, Proxy.NO_PROXY, dns.lookup(uriHost, 0), uriPort) - assertRoute(selection.next(), address, Proxy.NO_PROXY, dns.lookup(uriHost, 1), uriPort) + assertRoute(selection.next(), address, Proxy.NO_PROXY, dns[uriHost][0], uriPort) + assertRoute(selection.next(), address, Proxy.NO_PROXY, dns[uriHost][1], uriPort) assertThat(selection.hasNext()).isFalse() assertThat(routeSelector.hasNext()).isFalse() dns.assertRequests(uriHost) @@ -261,8 +261,8 @@ class RouteSelectorTest { assertThat(routeSelector.hasNext()).isTrue() dns[PROXY_A_HOST] = dns.allocate(2) val selection1 = routeSelector.next() - assertRoute(selection1.next(), address, proxyA, dns.lookup(PROXY_A_HOST, 0), PROXY_A_PORT) - assertRoute(selection1.next(), address, proxyA, dns.lookup(PROXY_A_HOST, 1), PROXY_A_PORT) + assertRoute(selection1.next(), address, proxyA, dns[PROXY_A_HOST][0], PROXY_A_PORT) + assertRoute(selection1.next(), address, proxyA, dns[PROXY_A_HOST][1], PROXY_A_PORT) dns.assertRequests(PROXY_A_HOST) assertThat(selection1.hasNext()).isFalse() @@ -270,7 +270,7 @@ class RouteSelectorTest { assertThat(routeSelector.hasNext()).isTrue() dns[PROXY_B_HOST] = dns.allocate(1) val selection2 = routeSelector.next() - assertRoute(selection2.next(), address, proxyB, dns.lookup(PROXY_B_HOST, 0), PROXY_B_PORT) + assertRoute(selection2.next(), address, proxyB, dns[PROXY_B_HOST][0], PROXY_B_PORT) dns.assertRequests(PROXY_B_HOST) assertThat(selection2.hasNext()).isFalse() @@ -288,7 +288,7 @@ class RouteSelectorTest { assertThat(routeSelector.hasNext()).isTrue() dns[uriHost] = dns.allocate(1) val selection = routeSelector.next() - assertRoute(selection.next(), address, Proxy.NO_PROXY, dns.lookup(uriHost, 0), uriPort) + assertRoute(selection.next(), address, Proxy.NO_PROXY, dns[uriHost][0], uriPort) dns.assertRequests(uriHost) assertThat(selection.hasNext()).isFalse() assertThat(routeSelector.hasNext()).isFalse() @@ -304,7 +304,7 @@ class RouteSelectorTest { assertThat(routeSelector.hasNext()).isTrue() dns[PROXY_A_HOST] = dns.allocate(1) val selection1 = routeSelector.next() - assertRoute(selection1.next(), address, proxyA, dns.lookup(PROXY_A_HOST, 0), PROXY_A_PORT) + assertRoute(selection1.next(), address, proxyA, dns[PROXY_A_HOST][0], PROXY_A_PORT) dns.assertRequests(PROXY_A_HOST) assertThat(selection1.hasNext()).isFalse() assertThat(routeSelector.hasNext()).isTrue() @@ -316,7 +316,7 @@ class RouteSelectorTest { assertThat(routeSelector.hasNext()).isTrue() dns[PROXY_A_HOST] = dns.allocate(1) val selection2 = routeSelector.next() - assertRoute(selection2.next(), address, proxyA, dns.lookup(PROXY_A_HOST, 0), PROXY_A_PORT) + assertRoute(selection2.next(), address, proxyA, dns[PROXY_A_HOST][0], PROXY_A_PORT) dns.assertRequests(PROXY_A_HOST) assertThat(selection2.hasNext()).isFalse() assertThat(routeSelector.hasNext()).isFalse() @@ -331,17 +331,17 @@ class RouteSelectorTest { // Proxy A dns[PROXY_A_HOST] = dns.allocate(2) val selection1 = routeSelector.next() - assertRoute(selection1.next(), address, proxyA, dns.lookup(PROXY_A_HOST, 0), PROXY_A_PORT) + assertRoute(selection1.next(), address, proxyA, dns[PROXY_A_HOST][0], PROXY_A_PORT) dns.assertRequests(PROXY_A_HOST) - assertRoute(selection1.next(), address, proxyA, dns.lookup(PROXY_A_HOST, 1), PROXY_A_PORT) + assertRoute(selection1.next(), address, proxyA, dns[PROXY_A_HOST][1], PROXY_A_PORT) assertThat(selection1.hasNext()).isFalse() // Proxy B dns[PROXY_B_HOST] = dns.allocate(2) val selection2 = routeSelector.next() - assertRoute(selection2.next(), address, proxyB, dns.lookup(PROXY_B_HOST, 0), PROXY_B_PORT) + assertRoute(selection2.next(), address, proxyB, dns[PROXY_B_HOST][0], PROXY_B_PORT) dns.assertRequests(PROXY_B_HOST) - assertRoute(selection2.next(), address, proxyB, dns.lookup(PROXY_B_HOST, 1), PROXY_B_PORT) + assertRoute(selection2.next(), address, proxyB, dns[PROXY_B_HOST][1], PROXY_B_PORT) assertThat(selection2.hasNext()).isFalse() // No more proxies to attempt. @@ -389,20 +389,20 @@ class RouteSelectorTest { val selection = routeSelector.next() dns.assertRequests(PROXY_A_HOST) val route = selection.next() - assertRoute(route, address, proxyA, dns.lookup(PROXY_A_HOST, 0), PROXY_A_PORT) + assertRoute(route, address, proxyA, dns[PROXY_A_HOST][0], PROXY_A_PORT) routeDatabase.failed(route) routeSelector = newRouteSelector(address) // Confirm we enumerate both proxies, giving preference to the route from ProxyB. val selection2 = routeSelector.next() dns.assertRequests(PROXY_A_HOST, PROXY_B_HOST) - assertRoute(selection2.next(), address, proxyB, dns.lookup(PROXY_B_HOST, 0), PROXY_B_PORT) + assertRoute(selection2.next(), address, proxyB, dns[PROXY_B_HOST][0], PROXY_B_PORT) assertThat(selection2.hasNext()).isFalse() // Confirm the last selection contains the postponed route from ProxyA. val selection3 = routeSelector.next() dns.assertRequests() - assertRoute(selection3.next(), address, proxyA, dns.lookup(PROXY_A_HOST, 0), PROXY_A_PORT) + assertRoute(selection3.next(), address, proxyA, dns[PROXY_A_HOST][0], PROXY_A_PORT) assertThat(selection3.hasNext()).isFalse() assertThat(routeSelector.hasNext()).isFalse() } @@ -414,8 +414,8 @@ class RouteSelectorTest { val selection = routeSelector.next() dns.assertRequests(uriHost) val routes = selection.routes - assertRoute(routes[0], address, Proxy.NO_PROXY, dns.lookup(uriHost, 0), uriPort) - assertRoute(routes[1], address, Proxy.NO_PROXY, dns.lookup(uriHost, 1), uriPort) + assertRoute(routes[0], address, Proxy.NO_PROXY, dns[uriHost][0], uriPort) + assertRoute(routes[1], address, Proxy.NO_PROXY, dns[uriHost][1], uriPort) assertThat(selection.next()).isSameInstanceAs(routes[0]) assertThat(selection.next()).isSameInstanceAs(routes[1]) assertThat(selection.hasNext()).isFalse() From 435528178cc925eecd64831f0dbfdebd6a6fd118 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Fri, 17 Jul 2026 14:30:06 -0400 Subject: [PATCH 3/3] Test DNS returning zero results --- okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt b/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt index c93dd45bc819..526e970fa4a1 100644 --- a/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt +++ b/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt @@ -115,16 +115,13 @@ class FakeDns : Dns { @Throws(UnknownHostException::class) override fun lookup(hostname: String): List { requests.put(Request.FunctionCall(hostname)) - - val result = get(hostname) - if (result.isEmpty()) throw UnknownHostException() - return result + return get(hostname) } /** Note that this request is not recorded. */ @Throws(UnknownHostException::class) operator fun get(hostname: String): List { - val records = data[hostname] ?: return listOf() + val records = data[hostname] ?: throw UnknownHostException() return records .filterIsInstance() .map { it.address }