From 7958452a6cbabe27ba7a04d1d195def59df05c77 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Fri, 17 Jul 2026 15:34:46 -0400 Subject: [PATCH 1/5] Start including ECH data in RouteSelector results This changes RouteSelector to use our new async API, when it is available. --- .../okhttp3/dnsoverhttps/DnsOverHttpsTest.kt | 4 + .../src/main/kotlin/okhttp3/FakeDns.kt | 106 +++++++++++++++++- .../okhttp3/internal/dns}/DnsTesting.kt | 33 +++++- .../commonJvmAndroid/kotlin/okhttp3/Dns.kt | 2 +- .../internal/connection/RouteSelector.kt | 103 ++++++++++++++--- .../okhttp3/internal/dns/-LookupDnsCall.kt | 3 +- .../internal/connection/RouteSelectorTest.kt | 49 ++++++-- 7 files changed, 273 insertions(+), 27 deletions(-) rename {okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps => okhttp-testing-support/src/main/kotlin/okhttp3/internal/dns}/DnsTesting.kt (70%) diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt index 411cb445ee67..186274cd0775 100644 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt @@ -52,6 +52,10 @@ import okhttp3.dnsoverhttps.internal.Question import okhttp3.dnsoverhttps.internal.ResourceRecord import okhttp3.dnsoverhttps.internal.TYPE_A import okhttp3.dnsoverhttps.internal.TYPE_AAAA +import okhttp3.internal.dns.DnsEvent +import okhttp3.internal.dns.EntryPoint +import okhttp3.internal.dns.execute +import okhttp3.internal.dns.invoke import okhttp3.testing.PlatformRule import okio.Buffer import okio.ByteString.Companion.decodeHex diff --git a/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt b/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt index 526e970fa4a1..7cb0d737d73d 100644 --- a/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt +++ b/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt @@ -17,6 +17,7 @@ package okhttp3 import assertk.assertThat import assertk.assertions.containsExactly +import java.io.IOException import java.net.Inet4Address import java.net.Inet6Address import java.net.InetAddress @@ -36,13 +37,16 @@ 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.internal.concurrent.TaskRunner import okio.Buffer import okio.ByteString.Companion.decodeBase64 /** * Handles DNS calls using in-memory records. */ -class FakeDns : Dns { +class FakeDns( + private val taskRunner: TaskRunner = TaskRunner.INSTANCE, +) : Dns { private val data = ConcurrentHashMap>() var extraHeaders: Headers = Headers.headersOf() @@ -112,6 +116,8 @@ class FakeDns : Dns { ) } + override fun newCall(request: Dns.Request): Dns.Call = FakeDnsCall(request) + @Throws(UnknownHostException::class) override fun lookup(hostname: String): List { requests.put(Request.FunctionCall(hostname)) @@ -216,6 +222,104 @@ class FakeDns : Dns { } } + /** + * Deliver each kind of DNS record in a separate callback, to most accurately simulate what a real + * DNS server does. + */ + private inner class FakeDnsCall( + override val request: Dns.Request, + ) : Dns.Call { + @Volatile private var canceled = false + private var executed = false + private val taskQueue = taskRunner.newQueue() + + override fun cancel() { + canceled = true + } + + override fun isCanceled() = canceled + + override fun enqueue(callback: Dns.Callback) { + check(!executed) + executed = true + + requests.put(Request.FunctionCall(request.hostname)) + + if (canceled) { + taskQueue.execute("${request.hostname} dns") { + callback.onFailure( + call = this, + e = IOException("canceled"), + ) + } + return + } + + val resourceRecords = data[request.hostname] ?: listOf() + if (resourceRecords.isEmpty()) { + taskQueue.execute("${request.hostname} dns") { + callback.onRecords( + call = this, + last = true, + records = listOf(), + ) + } + return + } + + val serviceMetadataRecords = mutableListOf() + val ipv6Records = mutableListOf() + val ipv4Records = mutableListOf() + for (resourceRecord in resourceRecords) { + when (resourceRecord) { + is ResourceRecord.Https -> { + serviceMetadataRecords += + Dns.Record.ServiceMetadata( + hostname = resourceRecord.targetName.takeIf { it != "" } ?: request.hostname, + port = resourceRecord.port, + alpnIds = resourceRecord.alpnIds?.map { Protocol.get(it) }, + ipAddressHints = resourceRecord.ipAddressHints, + echConfigList = resourceRecord.echConfigList, + ) + } + + is ResourceRecord.IpAddress -> { + val ipAddressRecord = Dns.Record.IpAddress(request.hostname, resourceRecord.address) + when (resourceRecord.address) { + is Inet4Address -> ipv4Records += ipAddressRecord + is Inet6Address -> ipv6Records += ipAddressRecord + else -> error("unexpected address") + } + } + } + } + + taskQueue.execute("${request.hostname} dns") { + if (serviceMetadataRecords.isNotEmpty()) { + callback.onRecords( + call = this, + last = ipv6Records.isEmpty() && ipv4Records.isEmpty(), + records = serviceMetadataRecords, + ) + } + if (ipv6Records.isNotEmpty()) { + callback.onRecords( + call = this, + last = ipv4Records.isEmpty(), + records = ipv6Records, + ) + } + if (ipv4Records.isNotEmpty()) { + callback.onRecords( + call = this, + last = true, + records = ipv4Records, + ) + } + } + } + } + sealed interface Request { val hostname: String diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsTesting.kt b/okhttp-testing-support/src/main/kotlin/okhttp3/internal/dns/DnsTesting.kt similarity index 70% rename from okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsTesting.kt rename to okhttp-testing-support/src/main/kotlin/okhttp3/internal/dns/DnsTesting.kt index 206db2c93d60..f8dda8c2d315 100644 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsTesting.kt +++ b/okhttp-testing-support/src/main/kotlin/okhttp3/internal/dns/DnsTesting.kt @@ -13,13 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package okhttp3.dnsoverhttps +package okhttp3.internal.dns import java.net.InetAddress import java.net.UnknownHostException import java.util.concurrent.BlockingDeque import java.util.concurrent.LinkedBlockingDeque import okhttp3.Dns +import okhttp3.dnsoverhttps.DnsOverHttps +import okhttp3.internal.concurrent.TaskRunner import okio.IOException sealed interface DnsEvent { @@ -33,7 +35,7 @@ sealed interface DnsEvent { ) : DnsEvent } -internal fun Dns.Call.execute(): BlockingDeque { +fun Dns.Call.execute(): BlockingDeque { val result = LinkedBlockingDeque() enqueue( @@ -98,6 +100,33 @@ operator fun DnsOverHttps.invoke( } } +/** + * Force this instance to use the lookup API or ([LookupDnsCall]), or the call API (and definitely + * not [LookupDnsCall]). This is for tests that want to defeat OkHttp's implementation detection, + * so we can exercise all code paths. + */ +fun Dns.forceEntryPoint(entryPoint: EntryPoint): Dns { + return when (entryPoint) { + EntryPoint.Lookup -> { + object : Dns by this { + override fun newCall(request: Dns.Request) = LookupDnsCall(TaskRunner.INSTANCE, this, request) + } + } + + EntryPoint.NewCall -> { + object : Dns by this { + override fun newCall(request: Dns.Request): Dns.Call { + val result = this@forceEntryPoint.newCall(request) + check(result !is LookupDnsCall) { + "unexpected call implementation on ${this@forceEntryPoint}" + } + return result + } + } + } + } +} + enum class EntryPoint { Lookup, NewCall, diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dns.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dns.kt index 1010c02dfcd0..51ea99b695fb 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dns.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dns.kt @@ -15,13 +15,13 @@ */ package okhttp3 +import java.io.IOException import java.net.InetAddress import java.net.UnknownHostException import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.dns.LookupDnsCall import okhttp3.internal.toCanonicalHost import okio.ByteString -import okio.IOException /** * Loads IP addresses and service metadata for a hostname. diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RouteSelector.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RouteSelector.kt index 385a8dc7665d..bd6af1ee4968 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RouteSelector.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RouteSelector.kt @@ -21,10 +21,13 @@ import java.net.InetSocketAddress import java.net.Proxy import java.net.SocketException import java.net.UnknownHostException +import java.util.concurrent.CompletableFuture import okhttp3.Address +import okhttp3.Dns import okhttp3.HttpUrl import okhttp3.Route import okhttp3.internal.canParseAsIpAddress +import okhttp3.internal.dns.LookupDnsCall import okhttp3.internal.immutableListOf import okhttp3.internal.toImmutableList @@ -185,33 +188,105 @@ class RouteSelector internal constructor( } } + // TODO: switch RouteSelector to be async. + + /** + * Use the new async [Dns.Call] API, but call it *synchronously* until we change this class to + * itself be asynchronous. + * + * To save threads and context switching, this currently falls back to the synchronous API if the + * DNS implementation is itself synchronous. When everything is async we won't do that anymore. + */ private fun dnsLookup( proxy: Proxy, socketHost: String, socketPort: Int, ): List { - call.eventListener.dnsStart(call, socketHost) + call.eventListener.dnsStart( + call = call, + domainName = socketHost, + ) + + val dnsRequest = Dns.Request(socketHost) + val result = + when (val dnsCall = address.dns.newCall(dnsRequest)) { + is LookupDnsCall -> { + val inetAddresses = address.dns.lookup(socketHost) + inetAddresses.map { inetAddress -> + Route( + address, + proxy, + InetSocketAddress(inetAddress, socketPort), + ) + } + } - // 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()) { + else -> { + val records = dnsCall.execute() + + val hostnameToServiceMetadata = + records + .filterIsInstance() + .associateBy { it.hostname } + + records + .filterIsInstance() + .map { record -> + val serviceMetadata = hostnameToServiceMetadata[record.hostname] + Route( + address = address, + proxy = proxy, + socketAddress = InetSocketAddress(record.address, socketPort), + echConfigList = serviceMetadata?.echConfigList, + ) + } + } + } + + if (result.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 = call, + domainName = socketHost, + inetAddressList = result.map { it.socketAddress.address }, + ) - call.eventListener.dnsEnd(call, socketHost, inetAddresses) return result } + /** Call this asynchronous API synchronously. */ + private fun Dns.Call.execute(): List { + val future = CompletableFuture>() + + enqueue( + object : Dns.Callback { + val allRecords = mutableListOf() + + override fun onRecords( + call: Dns.Call, + last: Boolean, + records: List, + ) { + allRecords += records + if (last) { + future.complete(allRecords) + } + } + + override fun onFailure( + call: Dns.Call, + e: okio.IOException, + ) { + future.completeExceptionally(e) + } + }, + ) + + return future.get() + } + /** A set of selected Routes. */ class Selection( val routes: List, diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-LookupDnsCall.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-LookupDnsCall.kt index c78fbeca6025..a76cbcca9bd9 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-LookupDnsCall.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-LookupDnsCall.kt @@ -33,7 +33,8 @@ import okhttp3.internal.testAndSet * When canceled, the callback is immediately notified but the in-flight call is run to completion * and discarded. */ -internal class LookupDnsCall( +@OkHttpInternalApi +class LookupDnsCall( private val taskRunner: TaskRunner, private val delegate: Dns, override val request: Dns.Request, diff --git a/okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/RouteSelectorTest.kt b/okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/RouteSelectorTest.kt index 266c82c14721..5809285b92ab 100644 --- a/okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/RouteSelectorTest.kt +++ b/okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/RouteSelectorTest.kt @@ -15,6 +15,7 @@ */ package okhttp3.internal.connection +import app.cash.burst.Burst import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEqualTo @@ -36,16 +37,23 @@ import okhttp3.OkHttpClientTestRule import okhttp3.Request import okhttp3.Route import okhttp3.TestValueFactory +import okhttp3.dnsoverhttps.internal.ResourceRecord import okhttp3.internal.connection.RouteSelector.Companion.socketHost +import okhttp3.internal.dns.EntryPoint +import okhttp3.internal.dns.forceEntryPoint import okhttp3.internal.http.RecordingProxySelector import okhttp3.testing.PlatformRule +import okio.ByteString import okio.ByteString.Companion.encodeUtf8 import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension -class RouteSelectorTest { +@Burst +class RouteSelectorTest( + private val entryPoint: EntryPoint = EntryPoint.NewCall, +) { @RegisterExtension val platform = PlatformRule() @@ -56,9 +64,10 @@ class RouteSelectorTest { private val proxySelector = RecordingProxySelector() private val uriHost = "hosta" private val uriPort = 1003 + private val echConfigList = "this is an encrypted client hello".encodeUtf8() private val factory = TestValueFactory().apply { - this.dns = this@RouteSelectorTest.dns + this.dns = this@RouteSelectorTest.dns.forceEntryPoint(entryPoint) this.proxySelector = this@RouteSelectorTest.proxySelector this.uriHost = this@RouteSelectorTest.uriHost this.uriPort = this@RouteSelectorTest.uriPort @@ -99,6 +108,28 @@ class RouteSelectorTest { } } + @Test fun routeReturnsEncryptedClientHelloData() { + val address = factory.newAddress() + val routeSelector = newRouteSelector(address) + assertThat(routeSelector.hasNext()).isTrue() + dns[uriHost] = + listOf( + ResourceRecord.IpAddress( + name = uriHost, + timeToLive = 5, + address = dns.allocate(1).single(), + ), + ResourceRecord.Https( + name = uriHost, + timeToLive = 5, + echConfigList = echConfigList, + ), + ) + val selection = routeSelector.next() + assertRoute(selection.next(), address, Proxy.NO_PROXY, dns[uriHost][0], uriPort, echConfigList) + dns.assertRequests(uriHost) + } + @Test fun singleRouteReturnsFailedRoute() { val address = factory.newAddress() var routeSelector = newRouteSelector(address) @@ -433,16 +464,16 @@ class RouteSelectorTest { fastFallback = false, ) assertThat(routeSelector.hasNext()).isTrue() - val (ipv4_1, ipv4_2) = dns.allocate(2) val (ipv6_1, ipv6_2) = dns.allocateIpv6(2) - dns[uriHost] = listOf(ipv4_1, ipv4_2, ipv6_1, ipv6_2) + val (ipv4_1, ipv4_2) = dns.allocate(2) + dns[uriHost] = listOf(ipv6_1, ipv6_2, ipv4_1, ipv4_2) val selection = routeSelector.next() assertThat(selection.routes.map { it.socketAddress.address }).containsExactly( - ipv4_1, - ipv4_2, ipv6_1, ipv6_2, + ipv4_1, + ipv4_2, ) } @@ -457,9 +488,9 @@ class RouteSelectorTest { fastFallback = true, ) assertThat(routeSelector.hasNext()).isTrue() - val (ipv4_1, ipv4_2) = dns.allocate(2) val (ipv6_1, ipv6_2) = dns.allocateIpv6(2) - dns[uriHost] = listOf(ipv4_1, ipv4_2, ipv6_1, ipv6_2) + val (ipv4_1, ipv4_2) = dns.allocate(2) + dns[uriHost] = listOf(ipv6_1, ipv6_2, ipv4_1, ipv4_2) val selection = routeSelector.next() assertThat(selection.routes.map { it.socketAddress.address }).containsExactly( @@ -574,11 +605,13 @@ class RouteSelectorTest { proxy: Proxy, socketAddress: InetAddress, socketPort: Int, + echConfigList: ByteString? = null, ) { assertThat(route.address).isEqualTo(address) assertThat(route.proxy).isEqualTo(proxy) assertThat(route.socketAddress.address).isEqualTo(socketAddress) assertThat(route.socketAddress.port).isEqualTo(socketPort) + assertThat(route.echConfigList).isEqualTo(echConfigList) } private fun newRouteSelector( From d0628ea659255f3f7bf664bd6075ebadc6940342 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Fri, 17 Jul 2026 15:42:33 -0400 Subject: [PATCH 2/5] Extract the execute() function --- .../okhttp3/dnsoverhttps/DnsOverHttpsTest.kt | 18 +++--- .../kotlin/okhttp3/internal/dns/DnsTesting.kt | 6 +- .../internal/connection/RouteSelector.kt | 33 +---------- .../okhttp3/internal/dns/-ExecuteDns.kt | 59 +++++++++++++++++++ 4 files changed, 72 insertions(+), 44 deletions(-) create mode 100644 okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-ExecuteDns.kt diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt index 186274cd0775..d3b4bc850e32 100644 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt @@ -54,8 +54,8 @@ import okhttp3.dnsoverhttps.internal.TYPE_A import okhttp3.dnsoverhttps.internal.TYPE_AAAA import okhttp3.internal.dns.DnsEvent import okhttp3.internal.dns.EntryPoint -import okhttp3.internal.dns.execute import okhttp3.internal.dns.invoke +import okhttp3.internal.dns.toEventsQueue import okhttp3.testing.PlatformRule import okio.Buffer import okio.ByteString.Companion.decodeHex @@ -359,7 +359,7 @@ class DnsOverHttpsTest( ) val call = dns.newCall(Dns.Request("lysine.dev")) - val dnsEvents = call.execute() + val dnsEvents = call.toEventsQueue() assertThat(dnsEvents.take()).isEqualTo( DnsEvent.Records( @@ -430,7 +430,7 @@ class DnsOverHttpsTest( ) val call = dns.newCall(Dns.Request("lysine.dev")) - val dnsEvents = call.execute() + val dnsEvents = call.toEventsQueue() assertThat(dnsEvents.take()).isEqualTo( DnsEvent.Records( @@ -478,7 +478,7 @@ class DnsOverHttpsTest( ) val call = dns.newCall(Dns.Request("lysine.dev")) - val dnsEvents = call.execute() + val dnsEvents = call.toEventsQueue() assertThat(dnsEvents.take()).isEqualTo( DnsEvent.Records( @@ -511,7 +511,7 @@ class DnsOverHttpsTest( ) val call = dns.newCall(Dns.Request("lysine.dev")) - val dnsEvents = call.execute() + val dnsEvents = call.toEventsQueue() assertThat(dnsEvents.take()).isEqualTo( DnsEvent.Records( @@ -534,7 +534,7 @@ class DnsOverHttpsTest( dns = buildLocalhost(bootstrapClient, includeIPv6 = true, includeHttps = true) val call = dns.newCall(Dns.Request("lysine.dev")) - val dnsEvents = call.execute() + val dnsEvents = call.toEventsQueue() assertThat(dnsEvents.take()).isEqualTo( DnsEvent.Records( @@ -553,7 +553,7 @@ class DnsOverHttpsTest( val call = dns.newCall(Dns.Request("lysine.dev")) call.cancel() assertThat(call.isCanceled()).isTrue() - val dnsEvents = call.execute() + val dnsEvents = call.toEventsQueue() assertThat(dnsEvents.take()).isInstanceOf() } @@ -572,7 +572,7 @@ class DnsOverHttpsTest( chain.proceed(chain.request()) } - val dnsEvents = call.execute() + val dnsEvents = call.toEventsQueue() assertThat(dnsEvents.take()).isInstanceOf() } @@ -605,7 +605,7 @@ class DnsOverHttpsTest( } } - val dnsEvents = call.execute() + val dnsEvents = call.toEventsQueue() assertThat(dnsEvents.take()).isEqualTo( DnsEvent.Records( last = false, diff --git a/okhttp-testing-support/src/main/kotlin/okhttp3/internal/dns/DnsTesting.kt b/okhttp-testing-support/src/main/kotlin/okhttp3/internal/dns/DnsTesting.kt index f8dda8c2d315..9b137e3952db 100644 --- a/okhttp-testing-support/src/main/kotlin/okhttp3/internal/dns/DnsTesting.kt +++ b/okhttp-testing-support/src/main/kotlin/okhttp3/internal/dns/DnsTesting.kt @@ -17,7 +17,7 @@ package okhttp3.internal.dns import java.net.InetAddress import java.net.UnknownHostException -import java.util.concurrent.BlockingDeque +import java.util.concurrent.BlockingQueue import java.util.concurrent.LinkedBlockingDeque import okhttp3.Dns import okhttp3.dnsoverhttps.DnsOverHttps @@ -35,7 +35,7 @@ sealed interface DnsEvent { ) : DnsEvent } -fun Dns.Call.execute(): BlockingDeque { +fun Dns.Call.toEventsQueue(): BlockingQueue { val result = LinkedBlockingDeque() enqueue( @@ -75,7 +75,7 @@ operator fun DnsOverHttps.invoke( EntryPoint.NewCall -> { buildList { - val dnsEvents = newCall(Dns.Request(hostname)).execute() + val dnsEvents = newCall(Dns.Request(hostname)).toEventsQueue() while (true) { when (val dnsEvent = dnsEvents.take()) { is DnsEvent.Failure -> { diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RouteSelector.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RouteSelector.kt index bd6af1ee4968..0bba12759fb4 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RouteSelector.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RouteSelector.kt @@ -21,13 +21,13 @@ import java.net.InetSocketAddress import java.net.Proxy import java.net.SocketException import java.net.UnknownHostException -import java.util.concurrent.CompletableFuture import okhttp3.Address import okhttp3.Dns import okhttp3.HttpUrl import okhttp3.Route import okhttp3.internal.canParseAsIpAddress import okhttp3.internal.dns.LookupDnsCall +import okhttp3.internal.dns.execute import okhttp3.internal.immutableListOf import okhttp3.internal.toImmutableList @@ -256,37 +256,6 @@ class RouteSelector internal constructor( return result } - /** Call this asynchronous API synchronously. */ - private fun Dns.Call.execute(): List { - val future = CompletableFuture>() - - enqueue( - object : Dns.Callback { - val allRecords = mutableListOf() - - override fun onRecords( - call: Dns.Call, - last: Boolean, - records: List, - ) { - allRecords += records - if (last) { - future.complete(allRecords) - } - } - - override fun onFailure( - call: Dns.Call, - e: okio.IOException, - ) { - future.completeExceptionally(e) - } - }, - ) - - return future.get() - } - /** A set of selected Routes. */ class Selection( val routes: List, diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-ExecuteDns.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-ExecuteDns.kt new file mode 100644 index 000000000000..f7fd202a4b00 --- /dev/null +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-ExecuteDns.kt @@ -0,0 +1,59 @@ +/* + * 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. + */ +@file:Suppress("ktlint:standard:filename") + +package okhttp3.internal.dns + +import java.util.concurrent.CompletableFuture +import okhttp3.Dns +import okhttp3.internal.OkHttpInternalApi + +/** + * Call our asynchronous API synchronously. + * + * This is intended to be transitional only; doing it this way needlessly blocks the caller's + * thread. + */ +@OkHttpInternalApi +fun Dns.Call.execute(): List { + val future = CompletableFuture>() + + enqueue( + object : Dns.Callback { + val allRecords = mutableListOf() + + override fun onRecords( + call: Dns.Call, + last: Boolean, + records: List, + ) { + allRecords += records + if (last) { + future.complete(allRecords) + } + } + + override fun onFailure( + call: Dns.Call, + e: okio.IOException, + ) { + future.completeExceptionally(e) + } + }, + ) + + return future.get() +} From 77babd6bbf113c6bd7439c04ab3d882125e0250f Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Fri, 17 Jul 2026 16:22:30 -0400 Subject: [PATCH 3/5] Fixup Https results to use targetName --- .../internal/-DnsOverHttpsCall.kt | 2 +- .../okhttp3/dnsoverhttps/DnsOverHttpsTest.kt | 52 ++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/internal/-DnsOverHttpsCall.kt b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/internal/-DnsOverHttpsCall.kt index 76ca09005d29..680c58e9ffd5 100644 --- a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/internal/-DnsOverHttpsCall.kt +++ b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/internal/-DnsOverHttpsCall.kt @@ -120,7 +120,7 @@ internal class DnsOverHttpsCall( when (resourceRecord) { is ResourceRecord.Https -> { Dns.Record.ServiceMetadata( - hostname = request.hostname, + hostname = resourceRecord.targetName.takeIf { it != "" } ?: request.hostname, alpnIds = resourceRecord.alpnIds?.mapNotNull { alpnId -> try { diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt index d3b4bc850e32..e9229a6396e9 100644 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt @@ -347,6 +347,7 @@ class DnsOverHttpsTest( ResourceRecord.Https( name = "lysine.dev", timeToLive = 5, + targetName = "cdn.lysine.dev", alpnIds = listOf(Protocol.HTTP_2.toString()), port = 8843, ipAddressHints = @@ -367,7 +368,7 @@ class DnsOverHttpsTest( records = listOf( Dns.Record.ServiceMetadata( - hostname = "lysine.dev", + hostname = "cdn.lysine.dev", alpnIds = listOf(Protocol.HTTP_2), port = 8843, ipAddressHints = @@ -406,6 +407,55 @@ class DnsOverHttpsTest( ) } + @Test + fun serviceMetadataEmptyTargetNameAliasesToRequestHostname() { + assumeTrue(entryPoint == EntryPoint.NewCall) + + dns = buildLocalhost(bootstrapClient, includeIPv6 = true, includeHttps = true) + server["lysine.dev"] = + listOf( + ResourceRecord.IpAddress( + name = "lysine.dev", + timeToLive = 5, + address = InetAddress.getByName("10.20.30.40"), + ), + ResourceRecord.Https( + name = "lysine.dev", + timeToLive = 5, + targetName = "", + alpnIds = listOf(Protocol.HTTP_2.toString()), + ), + ) + + val call = dns.newCall(Dns.Request("lysine.dev")) + val dnsEvents = call.toEventsQueue() + + assertThat(dnsEvents.take()).isEqualTo( + DnsEvent.Records( + last = false, + records = + listOf( + Dns.Record.ServiceMetadata( + hostname = "lysine.dev", + alpnIds = listOf(Protocol.HTTP_2), + ), + ), + ), + ) + assertThat(dnsEvents.take()).isEqualTo( + DnsEvent.Records( + last = true, + records = + listOf( + Dns.Record.IpAddress( + hostname = "lysine.dev", + address = InetAddress.getByName("10.20.30.40"), + ), + ), + ), + ) + } + /** An HTTPS error is received before the IPv4 results, but the error is delivered last. */ @Test fun httpsFailureIsDeliveredAfterIpv6AndIpv4Records() { From 09a894a25abbf3b9ecad88c1a493e69728208e05 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Fri, 17 Jul 2026 16:54:54 -0400 Subject: [PATCH 4/5] Fixup some tests --- .../src/jvmTest/kotlin/okhttp3/ConnectionCoalescingTest.kt | 2 +- .../kotlin/okhttp3/internal/connection/RouteSelectorTest.kt | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/okhttp/src/jvmTest/kotlin/okhttp3/ConnectionCoalescingTest.kt b/okhttp/src/jvmTest/kotlin/okhttp3/ConnectionCoalescingTest.kt index 67587aeffc57..345d71d0e5c3 100644 --- a/okhttp/src/jvmTest/kotlin/okhttp3/ConnectionCoalescingTest.kt +++ b/okhttp/src/jvmTest/kotlin/okhttp3/ConnectionCoalescingTest.kt @@ -84,7 +84,7 @@ class ConnectionCoalescingTest { .addSubjectAlternativeName("*.wildcard.com") .addSubjectAlternativeName("differentdns.com") .build() - serverIps = Dns.SYSTEM.lookup(server.hostName) + serverIps = listOf(server.socketAddress.address) dns[server.hostName] = serverIps dns["san.com"] = serverIps dns["nonsan.com"] = serverIps diff --git a/okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/RouteSelectorTest.kt b/okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/RouteSelectorTest.kt index 5809285b92ab..164c4d31a043 100644 --- a/okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/RouteSelectorTest.kt +++ b/okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/RouteSelectorTest.kt @@ -46,13 +46,14 @@ import okhttp3.testing.PlatformRule import okio.ByteString import okio.ByteString.Companion.encodeUtf8 import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assumptions.assumeTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension @Burst class RouteSelectorTest( - private val entryPoint: EntryPoint = EntryPoint.NewCall, + private val entryPoint: EntryPoint = EntryPoint.Lookup, ) { @RegisterExtension val platform = PlatformRule() @@ -109,6 +110,8 @@ class RouteSelectorTest( } @Test fun routeReturnsEncryptedClientHelloData() { + assumeTrue(entryPoint == EntryPoint.NewCall) + val address = factory.newAddress() val routeSelector = newRouteSelector(address) assertThat(routeSelector.hasNext()).isTrue() From 3a85bb080a81581cf3b1983720a42ae49a9f2cac Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Fri, 17 Jul 2026 18:17:33 -0400 Subject: [PATCH 5/5] Use LinkedBlockingQueue instead of CompletableFuture CompletableFuture isn't available until Android API 24 --- .../kotlin/okhttp3/internal/dns/-ExecuteDns.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-ExecuteDns.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-ExecuteDns.kt index f7fd202a4b00..c71f81b29f0c 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-ExecuteDns.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-ExecuteDns.kt @@ -17,7 +17,7 @@ package okhttp3.internal.dns -import java.util.concurrent.CompletableFuture +import java.util.concurrent.LinkedBlockingQueue import okhttp3.Dns import okhttp3.internal.OkHttpInternalApi @@ -29,7 +29,7 @@ import okhttp3.internal.OkHttpInternalApi */ @OkHttpInternalApi fun Dns.Call.execute(): List { - val future = CompletableFuture>() + val queue = LinkedBlockingQueue>>() enqueue( object : Dns.Callback { @@ -42,7 +42,7 @@ fun Dns.Call.execute(): List { ) { allRecords += records if (last) { - future.complete(allRecords) + queue.put(Result.success(allRecords)) } } @@ -50,10 +50,10 @@ fun Dns.Call.execute(): List { call: Dns.Call, e: okio.IOException, ) { - future.completeExceptionally(e) + queue.put(Result.failure(e)) } }, ) - return future.get() + return queue.take().getOrThrow() }