From f07c1da4bd136a0e0c327d96b5940f8c13e247ce Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Tue, 21 Jul 2026 10:01:41 -0400 Subject: [PATCH 1/3] Implement Dns.lookup on Dns.newCall for DnsOverHttps The lookup path is no longer exercised by OkHttp itself. This reduces potential drift between the two strategies. --- .../okhttp3/dnsoverhttps/DnsOverHttps.kt | 121 +++--------------- .../okhttp3/dnsoverhttps/DnsOverHttpsTest.kt | 53 +++++++- .../okhttp3/internal/dns/-ExecuteDns.kt | 20 ++- okhttp/src/jvmMain/java9/module-info.java | 1 + 4 files changed, 87 insertions(+), 108 deletions(-) diff --git a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt index 78120e32fc32..04677cb95ea1 100644 --- a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt +++ b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt @@ -15,28 +15,23 @@ */ package okhttp3.dnsoverhttps -import java.io.IOException import java.net.InetAddress import java.net.UnknownHostException -import java.util.concurrent.CountDownLatch import okhttp3.Call -import okhttp3.Callback import okhttp3.Dns import okhttp3.HttpUrl import okhttp3.MediaType 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.dns.execute import okhttp3.internal.publicsuffix.PublicSuffixDatabase /** @@ -96,101 +91,19 @@ class DnsOverHttps internal constructor( @Throws(UnknownHostException::class) override fun lookup(hostname: String): List { - val validationException = validate(hostname) - if (validationException != null) throw validationException - - val calls = callsList(hostname, inetAddressesOnly = true) - - val failures = ArrayList(3) - val results = ArrayList(5) - executeRequests(calls, results, failures) - - return results.ifEmpty { - throwBestFailure(hostname, failures) - } - } - - private fun executeRequests( - networkRequests: List, - responses: MutableList, - failures: MutableList, - ) { - val latch = CountDownLatch(networkRequests.size) - - for (call in networkRequests) { - call.enqueue( - object : Callback { - override fun onFailure( - call: Call, - e: IOException, - ) { - synchronized(failures) { - failures.add(e) - } - latch.countDown() - } - - override fun onResponse( - call: Call, - response: Response, - ) { - processResponse(response, responses, failures) - latch.countDown() - } - }, - ) - } - - try { - latch.await() - } catch (e: InterruptedException) { - failures.add(e) - } - } - - private fun processResponse( - response: Response, - results: MutableList, - failures: MutableList, - ) { - try { - val addresses = - decodeResponse(response) - .filterIsInstance() - .map { it.address } - synchronized(results) { - results.addAll(addresses) - } - } catch (e: IOException) { - synchronized(failures) { - failures.add(e) - } - } - } - - @Throws(UnknownHostException::class) - private fun throwBestFailure( - hostname: String, - failures: List, - ): List { - if (failures.isEmpty()) { - throw UnknownHostException(hostname) - } - - val failure = failures[0] - - if (failure is UnknownHostException) { - throw failure - } - - val unknownHostException = UnknownHostException(hostname) - unknownHostException.initCause(failure) - - for (i in 1 until failures.size) { - unknownHostException.addSuppressed(failures[i]) - } - - throw unknownHostException + val withoutServiceMetadata = DnsOverHttps( + client = client, + url = url, + includeIPv6 = includeIPv6, + includeServiceMetadata = false, + post = post, + resolvePrivateAddresses = resolvePrivateAddresses, + resolvePublicAddresses = resolvePublicAddresses, + ) + val call = withoutServiceMetadata.newCall(Dns.Request(hostname)) + val records = call.execute() + return records.filterIsInstance() + .map { it.address } } internal fun createCall( @@ -309,7 +222,8 @@ class DnsOverHttps internal constructor( this.bootstrapDnsHosts = bootstrapDnsHosts } - fun bootstrapDnsHosts(vararg bootstrapDnsHosts: InetAddress): Builder = bootstrapDnsHosts(bootstrapDnsHosts.toList()) + fun bootstrapDnsHosts(vararg bootstrapDnsHosts: InetAddress): Builder = + bootstrapDnsHosts(bootstrapDnsHosts.toList()) fun systemDns(systemDns: Dns) = apply { @@ -331,6 +245,7 @@ class DnsOverHttps internal constructor( } } - internal fun isPrivateHost(host: String): Boolean = PublicSuffixDatabase.get().getEffectiveTldPlusOne(host) == null + internal fun isPrivateHost(host: String): Boolean = + PublicSuffixDatabase.get().getEffectiveTldPlusOne(host) == null } } diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt index d32de35bf55b..2d99a7a1efbc 100644 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt @@ -85,7 +85,7 @@ import org.junit.jupiter.api.extension.RegisterExtension @Tag("Slowish") @Burst class DnsOverHttpsTest( - private val entryPoint: EntryPoint = EntryPoint.NewCall, + private val entryPoint: EntryPoint = EntryPoint.Lookup, ) { @RegisterExtension val platform = PlatformRule() @@ -180,7 +180,32 @@ class DnsOverHttpsTest( } @Test - fun failure() { + fun lookupDoesNotRequestServiceMetadata() { + assumeTrue(entryPoint == EntryPoint.Lookup) + + server["lysine.dev"] = + listOf( + InetAddress.getByName("10.20.30.40"), + InetAddress.getByName("1:2::3:4"), + ) + dns = buildLocalhost(bootstrapClient, includeIPv6 = true, includeServiceMetadata = true) + val result = dns(entryPoint, "lysine.dev") + assertThat(result).containsExactly( + address("1:2::3:4"), + address("10.20.30.40"), + ) + + val (_, dnsRequest1) = server.takeRequest() as DnsOverHttpsRequest + assertThat(dnsRequest1).isEqualTo(queryRequest("lysine.dev", TYPE_AAAA)) + + val (_, dnsRequest2) = server.takeRequest() as DnsOverHttpsRequest + assertThat(dnsRequest2).isEqualTo(queryRequest("lysine.dev", TYPE_A)) + + assertThat(server.pollRequest()).isNull() + } + + @Test + fun failsBecauseNoRecords() { assertFailsWith { dns(entryPoint, "lysine.dev") } @@ -190,6 +215,30 @@ class DnsOverHttpsTest( .isEqualTo(queryRequest("lysine.dev", TYPE_A)) } + @Test + fun lookupReturnsNormallyIfIpv4FailsAndIpv4Succeeds() { + assumeTrue(entryPoint == EntryPoint.Lookup) + + dns = buildLocalhost(bootstrapClient, includeIPv6 = true) + server["lysine.dev"] = listOf(InetAddress.getByName("11:22::33:44")) + server.sequenceIndexToOverride[1] = overrideResponse("") + + val results = dns(entryPoint, "lysine.dev") + assertThat(results).containsExactly(InetAddress.getByName("11:22::33:44")) + } + + @Test + fun lookupReturnsNormallyIfIpv6FailsAndIpv6Succeeds() { + assumeTrue(entryPoint == EntryPoint.Lookup) + + dns = buildLocalhost(bootstrapClient, includeIPv6 = true) + server["lysine.dev"] = listOf(InetAddress.getByName("10.20.30.40")) + server.sequenceIndexToOverride[0] = overrideResponse("") + + val results = dns(entryPoint, "lysine.dev") + assertThat(results).containsExactly(InetAddress.getByName("10.20.30.40")) + } + @Test fun resolveForbiddenPrivateDomain() { dns = buildLocalhost(bootstrapClient, resolvePrivateAddresses = false) diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-ExecuteDns.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-ExecuteDns.kt index c71f81b29f0c..c74338881d4e 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-ExecuteDns.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-ExecuteDns.kt @@ -17,6 +17,8 @@ package okhttp3.internal.dns +import java.io.IOException +import java.net.UnknownHostException import java.util.concurrent.LinkedBlockingQueue import okhttp3.Dns import okhttp3.internal.OkHttpInternalApi @@ -24,10 +26,14 @@ import okhttp3.internal.OkHttpInternalApi /** * Call our asynchronous API synchronously. * + * This returns normally if at least one [Dns.Record.IpAddress] is returned. Partial failures are + * silently ignored. + * * This is intended to be transitional only; doing it this way needlessly blocks the caller's * thread. */ @OkHttpInternalApi +@Throws(UnknownHostException::class) fun Dns.Call.execute(): List { val queue = LinkedBlockingQueue>>() @@ -42,15 +48,23 @@ fun Dns.Call.execute(): List { ) { allRecords += records if (last) { - queue.put(Result.success(allRecords)) + complete() } } override fun onFailure( call: Dns.Call, - e: okio.IOException, + e: IOException, ) { - queue.put(Result.failure(e)) + complete(e) + } + + private fun complete(e: IOException? = null) { + val result = when { + allRecords.any { it is Dns.Record.IpAddress } -> Result.success(allRecords) + else -> Result.failure(e ?: UnknownHostException()) + } + queue.put(result) } }, ) diff --git a/okhttp/src/jvmMain/java9/module-info.java b/okhttp/src/jvmMain/java9/module-info.java index be85fc417f13..bb08c78336ee 100644 --- a/okhttp/src/jvmMain/java9/module-info.java +++ b/okhttp/src/jvmMain/java9/module-info.java @@ -7,6 +7,7 @@ exports okhttp3.internal to okhttp3.logging, okhttp3.sse, okhttp3.java.net.cookiejar, okhttp3.dnsoverhttps, mockwebserver3, okhttp3.mockwebserver, mockwebserver3.junit5, okhttp3.coroutines, okhttp3.tls; exports okhttp3.internal.concurrent to mockwebserver3, okhttp3.mockwebserver; exports okhttp3.internal.connection to mockwebserver3, okhttp3.mockwebserver, okhttp3.logging; + exports okhttp3.internal.dns to okhttp3.dnsoverhttps; exports okhttp3.internal.http to okhttp3.logging, okhttp3.brotli, mockwebserver3; exports okhttp3.internal.http2 to mockwebserver3, okhttp3.mockwebserver; exports okhttp3.internal.platform to okhttp3.logging, okhttp3.java.net.cookiejar, okhttp3.dnsoverhttps, mockwebserver3, okhttp3.mockwebserver, okhttp3.tls; From 3c008c385b5f58c5ede9a7ff7e452b2216c06203 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Tue, 21 Jul 2026 11:29:38 -0400 Subject: [PATCH 2/3] Spotless --- .../okhttp3/dnsoverhttps/DnsOverHttps.kt | 28 +++++++++---------- .../okhttp3/internal/dns/-ExecuteDns.kt | 9 +++--- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt index 04677cb95ea1..e78d50dd40dc 100644 --- a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt +++ b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt @@ -91,18 +91,20 @@ class DnsOverHttps internal constructor( @Throws(UnknownHostException::class) override fun lookup(hostname: String): List { - val withoutServiceMetadata = DnsOverHttps( - client = client, - url = url, - includeIPv6 = includeIPv6, - includeServiceMetadata = false, - post = post, - resolvePrivateAddresses = resolvePrivateAddresses, - resolvePublicAddresses = resolvePublicAddresses, - ) + val withoutServiceMetadata = + DnsOverHttps( + client = client, + url = url, + includeIPv6 = includeIPv6, + includeServiceMetadata = false, + post = post, + resolvePrivateAddresses = resolvePrivateAddresses, + resolvePublicAddresses = resolvePublicAddresses, + ) val call = withoutServiceMetadata.newCall(Dns.Request(hostname)) val records = call.execute() - return records.filterIsInstance() + return records + .filterIsInstance() .map { it.address } } @@ -222,8 +224,7 @@ class DnsOverHttps internal constructor( this.bootstrapDnsHosts = bootstrapDnsHosts } - fun bootstrapDnsHosts(vararg bootstrapDnsHosts: InetAddress): Builder = - bootstrapDnsHosts(bootstrapDnsHosts.toList()) + fun bootstrapDnsHosts(vararg bootstrapDnsHosts: InetAddress): Builder = bootstrapDnsHosts(bootstrapDnsHosts.toList()) fun systemDns(systemDns: Dns) = apply { @@ -245,7 +246,6 @@ class DnsOverHttps internal constructor( } } - internal fun isPrivateHost(host: String): Boolean = - PublicSuffixDatabase.get().getEffectiveTldPlusOne(host) == null + internal fun isPrivateHost(host: String): Boolean = PublicSuffixDatabase.get().getEffectiveTldPlusOne(host) == null } } diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-ExecuteDns.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-ExecuteDns.kt index c74338881d4e..4b83adab2bff 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-ExecuteDns.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/-ExecuteDns.kt @@ -60,10 +60,11 @@ fun Dns.Call.execute(): List { } private fun complete(e: IOException? = null) { - val result = when { - allRecords.any { it is Dns.Record.IpAddress } -> Result.success(allRecords) - else -> Result.failure(e ?: UnknownHostException()) - } + val result = + when { + allRecords.any { it is Dns.Record.IpAddress } -> Result.success(allRecords) + else -> Result.failure(e ?: UnknownHostException()) + } queue.put(result) } }, From 9568fa1271288972aeb6f2eab480994bf6a839c4 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Tue, 21 Jul 2026 11:30:51 -0400 Subject: [PATCH 3/3] Update okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt Co-authored-by: Yuri Schimke --- .../src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt index 2d99a7a1efbc..968b2cb5d890 100644 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt @@ -216,7 +216,7 @@ class DnsOverHttpsTest( } @Test - fun lookupReturnsNormallyIfIpv4FailsAndIpv4Succeeds() { + fun lookupReturnsNormallyIfIpv4FailsAndIpv6Succeeds() { assumeTrue(entryPoint == EntryPoint.Lookup) dns = buildLocalhost(bootstrapClient, includeIPv6 = true)