From a2e4021e9f062a5ab8bc0f5506e1890d675c0ec7 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Wed, 15 Jul 2026 16:19:00 -0400 Subject: [PATCH 1/2] WIP: Implement Dns2 on DnsOverHttps Obviously this needs more than zero tests. --- .../api/okhttp-dnsoverhttps.api | 3 +- .../okhttp3/dnsoverhttps/-DnsOverHttpsCall.kt | 227 ++++++++++++++++++ .../okhttp3/dnsoverhttps/DnsOverHttps.kt | 6 +- 3 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsOverHttpsCall.kt diff --git a/okhttp-dnsoverhttps/api/okhttp-dnsoverhttps.api b/okhttp-dnsoverhttps/api/okhttp-dnsoverhttps.api index 6af96f4e9cea..6b827e94363c 100644 --- a/okhttp-dnsoverhttps/api/okhttp-dnsoverhttps.api +++ b/okhttp-dnsoverhttps/api/okhttp-dnsoverhttps.api @@ -1,10 +1,11 @@ -public final class okhttp3/dnsoverhttps/DnsOverHttps : okhttp3/Dns { +public final class okhttp3/dnsoverhttps/DnsOverHttps : okhttp3/Dns, okhttp3/Dns2 { public static final field Companion Lokhttp3/dnsoverhttps/DnsOverHttps$Companion; public static final field MAX_RESPONSE_SIZE I public final fun client ()Lokhttp3/OkHttpClient; public final fun includeHttps ()Z public final fun includeIPv6 ()Z public fun lookup (Ljava/lang/String;)Ljava/util/List; + public fun newCall (Lokhttp3/Dns2$Request;)Lokhttp3/Dns2$Call; public final fun post ()Z public final fun resolvePrivateAddresses ()Z public final fun resolvePublicAddresses ()Z diff --git a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsOverHttpsCall.kt b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsOverHttpsCall.kt new file mode 100644 index 000000000000..e10a384a295e --- /dev/null +++ b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsOverHttpsCall.kt @@ -0,0 +1,227 @@ +/* + * 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") +@file:OptIn(OkHttpInternalApi::class) + +package okhttp3.dnsoverhttps + +import java.io.IOException +import java.util.concurrent.atomic.AtomicReference +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Dns2 +import okhttp3.Protocol +import okhttp3.Response +import okhttp3.internal.OkHttpInternalApi +import okhttp3.internal.testAndSet + +// TODO: honor resolvePrivateAddresses, resolvePublicAddresses +// TODO: in-memory caching that uses timeToLive. +// TODO: honor Https.priority and Https.targetName. Create new calls! + +/** + * Implements [Dns2.Call] by making multiple HTTPS calls. + */ +internal class DnsOverHttpsCall( + private val dnsOverHttps: DnsOverHttps, + override val request: Dns2.Request, +) : Dns2.Call, + Callback { + private val state = AtomicReference(State.Idle) + + override fun enqueue(callback: Dns2.Callback) { + val calls = + buildList { + if (dnsOverHttps.includeHttps) { + add(dnsOverHttps.createCall(request.hostname, TYPE_HTTPS)) + } + + add(dnsOverHttps.createCall(request.hostname, TYPE_A)) + + if (dnsOverHttps.includeIPv6) { + add(dnsOverHttps.createCall(request.hostname, TYPE_AAAA)) + } + } + + val running = State.Running(callback, calls) + + val previous = state.testAndSet(running) { it is State.Idle } + when (previous) { + is State.Idle -> { + for (call in calls) { + call.enqueue(this) + } + } + + is State.Running, State.Complete -> { + throw IllegalStateException("already enqueued") + } + + State.Canceled -> { + callback.onFailure(this, IOException("canceled")) + } + } + } + + /** + * If this is the last DNS call, call [Callback.onFailure]. Otherwise, hold that call until the + * last DNS call completes. + */ + override fun onFailure( + call: Call, + e: IOException, + ) { + while (true) { + val previous = + state.get() as? State.Running + ?: return // Already complete or canceled; nothing to do. + + val newRunningCalls = previous.runningCalls - call + val allFailures = previous.delayedFailures + e + val next = + when { + newRunningCalls.isEmpty() -> { + State.Complete + } + + else -> { + State.Running( + callback = previous.callback, + runningCalls = newRunningCalls, + delayedFailures = allFailures, + ) + } + } + + if (!state.compareAndSet(previous, next)) continue // Lost a race; retry. + + if (next is State.Complete) { + previous.callback.onFailure( + call = this, + exceptions = allFailures, + ) + } + + return + } + } + + override fun onResponse( + call: Call, + response: Response, + ) { + val resourceRecords = + try { + dnsOverHttps.decodeResponse(response) + } catch (e: IOException) { + return onFailure(call, e) + } + + val dns2Records = + resourceRecords.map { resourceRecord -> + when (resourceRecord) { + is ResourceRecord.Https -> { + Dns2.Record.ServiceMetadata( + hostname = request.hostname, + alpnIds = + resourceRecord.alpnIds?.mapNotNull { alpnId -> + try { + Protocol.get(alpnId) + } catch (_: IOException) { + null // Skip unrecognized ALPN ID. + } + }, + port = resourceRecord.port, + ipAddressHints = resourceRecord.ipAddressHints, + echConfigList = resourceRecord.echConfigList, + ) + } + + is ResourceRecord.IpAddress -> { + Dns2.Record.IpAddress( + hostname = request.hostname, + address = resourceRecord.address, + ) + } + } + } + + while (true) { + val previous = + state.get() as? State.Running + ?: return // Already complete or canceled; nothing to do. + + val newRunningCalls = previous.runningCalls - call + val next = + when { + newRunningCalls.isEmpty() -> { + State.Complete + } + + else -> { + State.Running( + callback = previous.callback, + runningCalls = newRunningCalls, + delayedFailures = previous.delayedFailures, + ) + } + } + + if (!state.compareAndSet(previous, next)) continue // Lost a race; retry. + + previous.callback.onRecords( + call = this, + last = newRunningCalls.isEmpty(), + records = dns2Records, + ) + + return + } + } + + override fun cancel() { + val previous = state.testAndSet(State.Canceled) { it is State.Running || it == State.Idle } + (previous as? State.Running)?.callback?.onFailure(this, IOException("canceled")) + } + + override fun isCanceled() = state.get() is State.Canceled + + private sealed interface State { + object Idle : State + + class Running( + val callback: Dns2.Callback, + val runningCalls: List, + val delayedFailures: List = listOf(), + ) : State + + object Complete : State + + object Canceled : State + } +} + +internal fun Dns2.Callback.onFailure( + call: DnsOverHttpsCall, + exceptions: List, +) { + val firstException = exceptions.first() + for (i in 1 until exceptions.size) { + firstException.addSuppressed(exceptions[i]) + } + + onFailure(call, firstException) +} diff --git a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt index f91a796d3cbf..22222a30f4d2 100644 --- a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt +++ b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt @@ -23,6 +23,7 @@ import java.util.concurrent.CountDownLatch import okhttp3.Call import okhttp3.Callback import okhttp3.Dns +import okhttp3.Dns2 import okhttp3.HttpUrl import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaType @@ -51,7 +52,10 @@ class DnsOverHttps internal constructor( @get:JvmName("post") val post: Boolean, @get:JvmName("resolvePrivateAddresses") val resolvePrivateAddresses: Boolean, @get:JvmName("resolvePublicAddresses") val resolvePublicAddresses: Boolean, -) : Dns { +) : Dns, + Dns2 { + override fun newCall(request: Dns2.Request): Dns2.Call = DnsOverHttpsCall(this, request) + @Throws(UnknownHostException::class) override fun lookup(hostname: String): List { if (!resolvePrivateAddresses || !resolvePublicAddresses) { From 29d3c9d09a66953377ddee650ba2a08d7c5d7941 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Wed, 15 Jul 2026 23:27:07 -0400 Subject: [PATCH 2/2] Test the new API with the old tests --- okhttp-dnsoverhttps/build.gradle.kts | 1 + .../dnsoverhttps/DnsOverHttpsServer.kt | 18 ++ .../okhttp3/dnsoverhttps/DnsOverHttpsTest.kt | 225 +++++++++--------- 3 files changed, 136 insertions(+), 108 deletions(-) diff --git a/okhttp-dnsoverhttps/build.gradle.kts b/okhttp-dnsoverhttps/build.gradle.kts index ac661973a16f..4ef4d655d566 100644 --- a/okhttp-dnsoverhttps/build.gradle.kts +++ b/okhttp-dnsoverhttps/build.gradle.kts @@ -4,6 +4,7 @@ plugins { id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") + id("app.cash.burst") } project.applyOsgi( diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsServer.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsServer.kt index 2d12696c9707..de2aa53e02df 100644 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsServer.kt +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsServer.kt @@ -17,6 +17,7 @@ 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 mockwebserver3.Dispatcher @@ -43,6 +44,23 @@ internal class DnsOverHttpsServer : Dispatcher() { 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() diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt index 38d02c8a0d04..52b28f0c187c 100644 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt @@ -15,6 +15,7 @@ */ package okhttp3.dnsoverhttps +import app.cash.burst.Burst import assertk.assertThat import assertk.assertions.contains import assertk.assertions.containsExactly @@ -25,9 +26,10 @@ import assertk.assertions.isInstanceOf import assertk.assertions.isNull import java.io.EOFException import java.io.File -import java.io.IOException import java.net.InetAddress import java.net.UnknownHostException +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutionException import kotlin.reflect.KClass import kotlin.test.assertFailsWith import mockwebserver3.MockResponse @@ -37,7 +39,7 @@ import okhttp3.Cache import okhttp3.CallEvent import okhttp3.CallEvent.CacheHit import okhttp3.CallEvent.CacheMiss -import okhttp3.Dns +import okhttp3.Dns2 import okhttp3.EventRecorder import okhttp3.Headers.Companion.headersOf import okhttp3.OkHttpClient @@ -45,6 +47,7 @@ import okhttp3.Protocol import okhttp3.testing.PlatformRule import okio.Buffer import okio.ByteString.Companion.decodeHex +import okio.IOException import okio.Path.Companion.toPath import okio.fakefilesystem.FakeFileSystem import org.junit.jupiter.api.Assertions.fail @@ -54,7 +57,10 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension @Tag("Slowish") -class DnsOverHttpsTest { +@Burst +class DnsOverHttpsTest( + private val entryPoint: EntryPoint = EntryPoint.NewCall, +) { @RegisterExtension val platform = PlatformRule() @@ -67,7 +73,7 @@ class DnsOverHttpsTest { dispatcher = dnsOverHttpsServer } - private lateinit var dns: Dns + private lateinit var dns: DnsOverHttps private val cacheFs = FakeFileSystem() private val eventRecorder = EventRecorder() private val bootstrapClient = @@ -85,62 +91,47 @@ class DnsOverHttpsTest { @Test fun getOne() { - dnsOverHttpsServer["google.com"] = - listOf( - ResourceRecord.IpAddress( - name = "star.c10r.facebook.com", - timeToLive = 59, - address = InetAddress.getByName("157.240.1.18"), - ), - ) - val result = dns.lookup("google.com") - assertThat(result).isEqualTo(listOf(address("157.240.1.18"))) + dnsOverHttpsServer["lysine.dev"] = listOf(InetAddress.getByName("10.20.30.40")) + val result = dns.invoke("lysine.dev") + assertThat(result).isEqualTo(listOf(address("10.20.30.40"))) val (httpsRequest, dnsRequest) = dnsOverHttpsServer.takeRequest() assertThat(httpsRequest.method).isEqualTo("GET") assertThat(dnsRequest) - .isEqualTo(queryRequest("google.com", TYPE_A)) + .isEqualTo(queryRequest("lysine.dev", TYPE_A)) } @Test fun getIpv6() { - dnsOverHttpsServer["google.com"] = + dnsOverHttpsServer["lysine.dev"] = listOf( - ResourceRecord.IpAddress( - name = "star.c10r.facebook.com", - timeToLive = 59, - address = InetAddress.getByName("157.240.1.18"), - ), - ResourceRecord.IpAddress( - name = "star.c10r.facebook.com", - timeToLive = 59, - address = InetAddress.getByName("2a03:2880:f029:11:face:b00c:0:2"), - ), + InetAddress.getByName("10.20.30.40"), + InetAddress.getByName("1:2::3:4"), ) dns = buildLocalhost(bootstrapClient, true) - val result = dns.lookup("google.com") + val result = dns("lysine.dev") assertThat(result.size).isEqualTo(2) - assertThat(result).contains(address("157.240.1.18")) - assertThat(result).contains(address("2a03:2880:f029:11:face:b00c:0:2")) + assertThat(result).contains(address("10.20.30.40")) + assertThat(result).contains(address("1:2::3:4")) val (httpsRequest1, dnsRequest1) = dnsOverHttpsServer.takeRequest() assertThat(httpsRequest1.method).isEqualTo("GET") val (httpsRequest2, dnsRequest2) = dnsOverHttpsServer.takeRequest() assertThat(httpsRequest2.method).isEqualTo("GET") assertThat(listOf(dnsRequest1, dnsRequest2)) .containsExactlyInAnyOrder( - queryRequest("google.com", TYPE_A), - queryRequest("google.com", TYPE_AAAA), + queryRequest("lysine.dev", TYPE_A), + queryRequest("lysine.dev", TYPE_AAAA), ) } @Test fun failure() { assertFailsWith { - dns.lookup("google.com") + dns("lysine.dev") } val (httpsRequest, dnsRequest) = dnsOverHttpsServer.takeRequest() assertThat(httpsRequest.method).isEqualTo("GET") assertThat(dnsRequest) - .isEqualTo(queryRequest("google.com", TYPE_A)) + .isEqualTo(queryRequest("lysine.dev", TYPE_A)) } @Test @@ -148,26 +139,23 @@ class DnsOverHttpsTest { val array = CharArray(128 * 1024 + 2) { '0' } dnsOverHttpsServer.override = overrideResponse(String(array)) try { - dns.lookup("google.com") + dns("lysine.dev") fail() - } catch (ioe: IOException) { - assertThat(ioe.message).isEqualTo("google.com") - val cause = ioe.cause!! - assertThat(cause).isInstanceOf() - assertThat(cause).hasMessage("response size exceeds limit (65536 bytes): 65537 bytes") + } catch (e: IOException) { + val rootCause = e.cause ?: e + assertThat(rootCause).hasMessage("response size exceeds limit (65536 bytes): 65537 bytes") } } @Test fun failOnBadResponse() { dnsOverHttpsServer.override = overrideResponse("00") - try { - dns.lookup("google.com") - fail() - } catch (ioe: IOException) { - assertThat(ioe).hasMessage("google.com") - assertThat(ioe.cause!!).isInstanceOf() - } + val e = + assertFailsWith { + dns("lysine.dev") + } + val rootCause = e.cause ?: e + assertThat(rootCause).isInstanceOf() } // TODO GET preferred order - with tests to confirm this @@ -187,44 +175,30 @@ class DnsOverHttpsTest { "cache-control", "private, max-age=298", ) - dnsOverHttpsServer["google.com"] = - listOf( - ResourceRecord.IpAddress( - name = "star.c10r.facebook.com", - timeToLive = 59, - address = InetAddress.getByName("157.240.1.18"), - ), - ) - dnsOverHttpsServer["www.google.com"] = - listOf( - ResourceRecord.IpAddress( - name = "star.c10r.facebook.com", - timeToLive = 59, - address = InetAddress.getByName("157.240.1.18"), - ), - ) + dnsOverHttpsServer["lysine.dev"] = listOf(InetAddress.getByName("10.20.30.40")) + dnsOverHttpsServer["alternate.lysine.dev"] = listOf(InetAddress.getByName("55.66.77.88")) - var result = cachedDns.lookup("google.com") - assertThat(result).containsExactly(address("157.240.1.18")) + val result1 = cachedDns("lysine.dev") + assertThat(result1).containsExactly(address("10.20.30.40")) val (httpsRequest1, dnsRequest1) = dnsOverHttpsServer.takeRequest() assertThat(httpsRequest1.method).isEqualTo("GET") assertThat(dnsRequest1) - .isEqualTo(queryRequest("google.com", TYPE_A)) + .isEqualTo(queryRequest("lysine.dev", TYPE_A)) assertThat(cacheEvents()).containsExactly(CacheMiss::class) - result = cachedDns.lookup("google.com") + val result2 = cachedDns("lysine.dev") assertThat(dnsOverHttpsServer.pollRequest()).isNull() - assertThat(result).isEqualTo(listOf(address("157.240.1.18"))) + assertThat(result2).isEqualTo(listOf(address("10.20.30.40"))) assertThat(cacheEvents()).containsExactly(CacheHit::class) - result = cachedDns.lookup("www.google.com") - assertThat(result).containsExactly(address("157.240.1.18")) + val result3 = cachedDns("alternate.lysine.dev") + assertThat(result3).containsExactly(address("55.66.77.88")) val (httpsRequest2, dnsRequest2) = dnsOverHttpsServer.takeRequest() assertThat(httpsRequest2.method).isEqualTo("GET") assertThat(dnsRequest2) - .isEqualTo(queryRequest("www.google.com", TYPE_A)) + .isEqualTo(queryRequest("alternate.lysine.dev", TYPE_A)) assertThat(cacheEvents()).containsExactly(CacheMiss::class) } @@ -239,25 +213,11 @@ class DnsOverHttpsTest { "cache-control", "private, max-age=298", ) - dnsOverHttpsServer["google.com"] = - listOf( - ResourceRecord.IpAddress( - name = "star.c10r.facebook.com", - timeToLive = 59, - address = InetAddress.getByName("157.240.1.18"), - ), - ) - dnsOverHttpsServer["www.google.com"] = - listOf( - ResourceRecord.IpAddress( - name = "star.c10r.facebook.com", - timeToLive = 59, - address = InetAddress.getByName("157.240.1.18"), - ), - ) + dnsOverHttpsServer["lysine.dev"] = listOf(InetAddress.getByName("10.20.30.40")) + dnsOverHttpsServer["alternate.lysine.dev"] = listOf(InetAddress.getByName("55.66.77.88")) - var result = cachedDns.lookup("google.com") - assertThat(result).containsExactly(address("157.240.1.18")) + val result1 = cachedDns("lysine.dev") + assertThat(result1).containsExactly(address("10.20.30.40")) val (httpsRequest1, _) = dnsOverHttpsServer.takeRequest() assertThat(httpsRequest1.method).isEqualTo("POST") assertThat(httpsRequest1.url.encodedQuery) @@ -265,14 +225,14 @@ class DnsOverHttpsTest { assertThat(cacheEvents()).containsExactly(CacheMiss::class) - result = cachedDns.lookup("google.com") + val result2 = cachedDns("lysine.dev") assertThat(dnsOverHttpsServer.pollRequest()).isNull() - assertThat(result).isEqualTo(listOf(address("157.240.1.18"))) + assertThat(result2).isEqualTo(listOf(address("10.20.30.40"))) assertThat(cacheEvents()).containsExactly(CacheHit::class) - result = cachedDns.lookup("www.google.com") - assertThat(result).containsExactly(address("157.240.1.18")) + val result3 = cachedDns("alternate.lysine.dev") + assertThat(result3).containsExactly(address("55.66.77.88")) val (httpsRequest2, _) = dnsOverHttpsServer.takeRequest() assertThat(httpsRequest2.method).isEqualTo("POST") assertThat(httpsRequest2.url.encodedQuery) @@ -289,32 +249,24 @@ class DnsOverHttpsTest { dnsOverHttpsServer.extraHeaders = headersOf( "cache-control", - "max-age=1", - ) - dnsOverHttpsServer["google.com"] = - listOf( - ResourceRecord.IpAddress( - name = "star.c10r.facebook.com", - timeToLive = 59, - address = InetAddress.getByName("157.240.1.18"), - ), + "no-store", ) - var result = cachedDns.lookup("google.com") - assertThat(result).containsExactly(address("157.240.1.18")) + dnsOverHttpsServer["lysine.dev"] = listOf(InetAddress.getByName("10.20.30.40")) + val result1 = cachedDns("lysine.dev") + assertThat(result1).containsExactly(address("10.20.30.40")) val (httpsRequest1, dnsRequest1) = dnsOverHttpsServer.takeRequest() assertThat(httpsRequest1.method).isEqualTo("GET") assertThat(dnsRequest1) - .isEqualTo(queryRequest("google.com", TYPE_A)) + .isEqualTo(queryRequest("lysine.dev", TYPE_A)) assertThat(cacheEvents()).containsExactly(CacheMiss::class) - Thread.sleep(2000) - result = cachedDns.lookup("google.com") - assertThat(result).isEqualTo(listOf(address("157.240.1.18"))) + val result2 = cachedDns("lysine.dev") + assertThat(result2).isEqualTo(listOf(address("10.20.30.40"))) val (httpsRequest2, dnsRequest2) = dnsOverHttpsServer.takeRequest() assertThat(httpsRequest2.method).isEqualTo("GET") assertThat(dnsRequest2) - .isEqualTo(queryRequest("google.com", TYPE_A)) + .isEqualTo(queryRequest("lysine.dev", TYPE_A)) assertThat(cacheEvents()).containsExactly(CacheMiss::class) } @@ -366,6 +318,63 @@ class DnsOverHttpsTest { ), ) + /** + * Use Burst to call either the blocking API or the non-blocking API, both with a signature that + * resembles the [InetAddress.getAllByName] API. + */ + private operator fun DnsOverHttps.invoke(hostname: String): List = + when (entryPoint) { + EntryPoint.Lookup -> { + lookup(hostname) + } + + EntryPoint.NewCall -> { + val future = CompletableFuture>() + + val call = newCall(Dns2.Request(hostname)) + call.enqueue( + object : Dns2.Callback { + private val results = mutableListOf() + + override fun onRecords( + call: Dns2.Call, + last: Boolean, + records: List, + ) { + this.results += + records + .filterIsInstance() + .map { it.address } + if (last) { + when { + results.isNotEmpty() -> future.complete(this.results) + else -> future.completeExceptionally(UnknownHostException()) + } + } + } + + override fun onFailure( + call: Dns2.Call, + e: IOException, + ) { + future.completeExceptionally(e) + } + }, + ) + + try { + future.get() + } catch (e: ExecutionException) { + throw e.cause!! + } + } + } + + enum class EntryPoint { + Lookup, + NewCall, + } + companion object { private fun address(host: String) = InetAddress.getByName(host) }