From a290ad79605f875f49d243b6e2c28b7fbf2ae2f0 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Wed, 15 Jul 2026 16:13:18 -0400 Subject: [PATCH 1/2] Change how we test DnsOverHttps Rather than returning opaque hard-coded response bodies, use our new message encoder so we can test with real value objects. --- .../okhttp3/dnsoverhttps/DnsOverHttps.kt | 86 +++--- .../dnsoverhttps/DnsOverHttpsServer.kt | 120 ++++++++ .../okhttp3/dnsoverhttps/DnsOverHttpsTest.kt | 266 ++++++++++-------- .../kotlin/okhttp3/internal/-UtilJvm.kt | 3 +- .../okhttp3/internal/OkHttpInternalApi.kt | 27 ++ .../kotlin/okhttp3/internal/dns/DnsAsDns2.kt | 3 + 6 files changed, 341 insertions(+), 164 deletions(-) create mode 100644 okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsServer.kt create mode 100644 okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/OkHttpInternalApi.kt diff --git a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt index 634653f65983..f91a796d3cbf 100644 --- a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt +++ b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt @@ -71,22 +71,17 @@ class DnsOverHttps internal constructor( @Throws(UnknownHostException::class) private fun lookupHttps(hostname: String): List { - val networkRequests = + val calls = buildList { - add(client.newCall(buildRequest(hostname, TYPE_A))) - - if (includeHttps) { - add(client.newCall(buildRequest(hostname, TYPE_HTTPS))) - } - + add(createCall(hostname, TYPE_A)) if (includeIPv6) { - add(client.newCall(buildRequest(hostname, TYPE_AAAA))) + add(createCall(hostname, TYPE_AAAA)) } } val failures = ArrayList(3) val results = ArrayList(5) - executeRequests(networkRequests, results, failures) + executeRequests(calls, results, failures) return results.ifEmpty { throwBestFailure(hostname, failures) @@ -137,7 +132,10 @@ class DnsOverHttps internal constructor( failures: MutableList, ) { try { - val addresses = readResponse(response) + val addresses = + decodeResponse(response) + .filterIsInstance() + .map { it.address } synchronized(results) { results.addAll(addresses) } @@ -174,7 +172,7 @@ class DnsOverHttps internal constructor( } @Throws(IOException::class) - private fun readResponse(response: Response): List { + internal fun decodeResponse(response: Response): List { if ( response.cacheResponse == null && response.protocol !== Protocol.HTTP_2 && @@ -197,46 +195,44 @@ class DnsOverHttps internal constructor( val dnsResponse = DnsMessageReader(body.source()).read() when (dnsResponse.responseCode) { - RESPONSE_CODE_SUCCESS -> { - return dnsResponse.answers - .filterIsInstance() - .map { it.address } - } - - RESPONSE_CODE_SERVER_FAILURE -> { - throw UnknownHostException("DNS server failure") - } - - else -> { - throw UnknownHostException() - } + RESPONSE_CODE_SUCCESS -> return dnsResponse.answers + RESPONSE_CODE_SERVER_FAILURE -> throw UnknownHostException("DNS server failure") + else -> throw UnknownHostException() } } } - private fun buildRequest( + internal fun createCall( hostname: String, type: Int, - ): Request = - Request - .Builder() - .header("Accept", DNS_MESSAGE.toString()) - .apply { - val dnsUrl: HttpUrl = this@DnsOverHttps.url - if (post) { - url(dnsUrl) - .cacheUrlOverride( - dnsUrl - .newBuilder() - .addQueryParameter("hostname", hostname) - .build(), - ).post(QueryRequestBody(DnsMessage.query(hostname, type))) - } else { - val queryParameter = DnsMessage.query(hostname, type).asQueryParameter() - val requestUrl = dnsUrl.newBuilder().addQueryParameter("dns", queryParameter).build() - url(requestUrl) - } - }.build() + ): Call = + client.newCall( + request = + Request + .Builder() + .header("Accept", DNS_MESSAGE.toString()) + .apply { + val dnsUrl = this@DnsOverHttps.url + if (post) { + url(dnsUrl) + cacheUrlOverride( + dnsUrl + .newBuilder() + .addQueryParameter("hostname", hostname) + .build(), + ) + post(QueryRequestBody(DnsMessage.query(hostname, type))) + } else { + val queryParameter = DnsMessage.query(hostname, type).asQueryParameter() + val requestUrl = + dnsUrl + .newBuilder() + .addQueryParameter("dns", queryParameter) + .build() + url(requestUrl) + } + }.build(), + ) class Builder { internal var client: OkHttpClient? = null diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsServer.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsServer.kt new file mode 100644 index 000000000000..2d12696c9707 --- /dev/null +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsServer.kt @@ -0,0 +1,120 @@ +/* + * 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.util.concurrent.ConcurrentHashMap +import java.util.concurrent.LinkedBlockingDeque +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>() + var override: MockResponse? = null + + operator fun set( + hostname: String, + records: List, + ) { + data[hostname] = records + } + + fun takeRequest(): Pair = requests.take() + + fun pollRequest(): Pair? = requests.poll() + + override fun dispatch(request: RecordedRequest): MockResponse { + val override = this.override + 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 a304a6a7813f..38d02c8a0d04 100644 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt @@ -28,7 +28,6 @@ import java.io.File import java.io.IOException import java.net.InetAddress import java.net.UnknownHostException -import java.util.concurrent.TimeUnit import kotlin.reflect.KClass import kotlin.test.assertFailsWith import mockwebserver3.MockResponse @@ -40,6 +39,7 @@ import okhttp3.CallEvent.CacheHit import okhttp3.CallEvent.CacheMiss import okhttp3.Dns import okhttp3.EventRecorder +import okhttp3.Headers.Companion.headersOf import okhttp3.OkHttpClient import okhttp3.Protocol import okhttp3.testing.PlatformRule @@ -58,8 +58,14 @@ class DnsOverHttpsTest { @RegisterExtension val platform = PlatformRule() + private val dnsOverHttpsServer = DnsOverHttpsServer() + @StartStop - private val server = MockWebServer() + private val server = + MockWebServer() + .apply { + dispatcher = dnsOverHttpsServer + } private lateinit var dns: Dns private val cacheFs = FakeFileSystem() @@ -79,75 +85,68 @@ class DnsOverHttpsTest { @Test fun getOne() { - server.enqueue( - dnsResponse( - "0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c000500010" + - "0000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010001000" + - "0003b00049df00112", - ), - ) + 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"))) - val recordedRequest = server.takeRequest() - assertThat(recordedRequest.method).isEqualTo("GET") - assertThat(recordedRequest.url.encodedQuery) - .isEqualTo("ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ") + val (httpsRequest, dnsRequest) = dnsOverHttpsServer.takeRequest() + assertThat(httpsRequest.method).isEqualTo("GET") + assertThat(dnsRequest) + .isEqualTo(queryRequest("google.com", TYPE_A)) } @Test fun getIpv6() { - server.enqueue( - dnsResponse( - "0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c0005000" + - "100000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c0420001000" + - "10000003b00049df00112", - ), - ) - server.enqueue( - dnsResponse( - "0000818000010003000000000567726170680866616365626f6f6b03636f6d00001c0001c00c0005000" + - "100000a1b000603617069c012c0300005000100000b1f000c04737461720463313072c012c042001c000" + - "10000003b00102a032880f0290011faceb00c00000002", - ), - ) + dnsOverHttpsServer["google.com"] = + 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"), + ), + ) dns = buildLocalhost(bootstrapClient, true) val result = dns.lookup("google.com") 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")) - val request1 = server.takeRequest() - assertThat(request1.method).isEqualTo("GET") - val request2 = server.takeRequest() - assertThat(request2.method).isEqualTo("GET") - assertThat(listOf(request1.url.encodedQuery, request2.url.encodedQuery)) + 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( - "ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ", - "ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AABwAAQ", + queryRequest("google.com", TYPE_A), + queryRequest("google.com", TYPE_AAAA), ) } @Test fun failure() { - server.enqueue( - dnsResponse( - "0000818300010000000100000e7364666c6b686673646c6b6a64660265650000010001c01b00060001000" + - "007070038026e7303746c64c01b0a686f73746d61737465720d6565737469696e7465726e6574c01b5adb1" + - "2c100000e10000003840012750000000e10", - ), - ) assertFailsWith { dns.lookup("google.com") } - val recordedRequest = server.takeRequest() - assertThat(recordedRequest.method).isEqualTo("GET") - assertThat(recordedRequest.url.encodedQuery) - .isEqualTo("ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ") + val (httpsRequest, dnsRequest) = dnsOverHttpsServer.takeRequest() + assertThat(httpsRequest.method).isEqualTo("GET") + assertThat(dnsRequest) + .isEqualTo(queryRequest("google.com", TYPE_A)) } @Test fun failOnExcessiveResponse() { val array = CharArray(128 * 1024 + 2) { '0' } - server.enqueue(dnsResponse(String(array))) + dnsOverHttpsServer.override = overrideResponse(String(array)) try { dns.lookup("google.com") fail() @@ -161,7 +160,7 @@ class DnsOverHttpsTest { @Test fun failOnBadResponse() { - server.enqueue(dnsResponse("00")) + dnsOverHttpsServer.override = overrideResponse("00") try { dns.lookup("google.com") fail() @@ -183,39 +182,49 @@ class DnsOverHttpsTest { val cachedClient = bootstrapClient.newBuilder().cache(cache).build() val cachedDns = buildLocalhost(cachedClient, false) - repeat(2) { - server.enqueue( - dnsResponse( - "0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c000500010" + - "0000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010001000" + - "0003b00049df00112", - ).newBuilder() - .setHeader("cache-control", "private, max-age=298") - .build(), + dnsOverHttpsServer.extraHeaders = + headersOf( + "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"), + ), ) - } var result = cachedDns.lookup("google.com") assertThat(result).containsExactly(address("157.240.1.18")) - var recordedRequest = server.takeRequest() - assertThat(recordedRequest.method).isEqualTo("GET") - assertThat(recordedRequest.url.encodedQuery) - .isEqualTo("ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ") + val (httpsRequest1, dnsRequest1) = dnsOverHttpsServer.takeRequest() + assertThat(httpsRequest1.method).isEqualTo("GET") + assertThat(dnsRequest1) + .isEqualTo(queryRequest("google.com", TYPE_A)) assertThat(cacheEvents()).containsExactly(CacheMiss::class) result = cachedDns.lookup("google.com") - assertThat(server.takeRequest(1, TimeUnit.MILLISECONDS)).isNull() + assertThat(dnsOverHttpsServer.pollRequest()).isNull() assertThat(result).isEqualTo(listOf(address("157.240.1.18"))) assertThat(cacheEvents()).containsExactly(CacheHit::class) result = cachedDns.lookup("www.google.com") assertThat(result).containsExactly(address("157.240.1.18")) - recordedRequest = server.takeRequest() - assertThat(recordedRequest.method).isEqualTo("GET") - assertThat(recordedRequest.url.encodedQuery) - .isEqualTo("ct&dns=AAABAAABAAAAAAAAA3d3dwZnb29nbGUDY29tAAABAAE") + val (httpsRequest2, dnsRequest2) = dnsOverHttpsServer.takeRequest() + assertThat(httpsRequest2.method).isEqualTo("GET") + assertThat(dnsRequest2) + .isEqualTo(queryRequest("www.google.com", TYPE_A)) assertThat(cacheEvents()).containsExactly(CacheMiss::class) } @@ -225,38 +234,48 @@ class DnsOverHttpsTest { val cache = Cache(cacheFs, "cache".toPath(), (100 * 1024).toLong()) val cachedClient = bootstrapClient.newBuilder().cache(cache).build() val cachedDns = buildLocalhost(cachedClient, false, post = true) - repeat(2) { - server.enqueue( - dnsResponse( - "0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c000500010" + - "0000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010001000" + - "0003b00049df00112", - ).newBuilder() - .setHeader("cache-control", "private, max-age=298") - .build(), + dnsOverHttpsServer.extraHeaders = + headersOf( + "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"), + ), ) - } var result = cachedDns.lookup("google.com") assertThat(result).containsExactly(address("157.240.1.18")) - var recordedRequest = server.takeRequest() - assertThat(recordedRequest.method).isEqualTo("POST") - assertThat(recordedRequest.url.encodedQuery) + val (httpsRequest1, _) = dnsOverHttpsServer.takeRequest() + assertThat(httpsRequest1.method).isEqualTo("POST") + assertThat(httpsRequest1.url.encodedQuery) .isEqualTo("ct") assertThat(cacheEvents()).containsExactly(CacheMiss::class) result = cachedDns.lookup("google.com") - assertThat(server.takeRequest(0, TimeUnit.MILLISECONDS)).isNull() + assertThat(dnsOverHttpsServer.pollRequest()).isNull() assertThat(result).isEqualTo(listOf(address("157.240.1.18"))) assertThat(cacheEvents()).containsExactly(CacheHit::class) result = cachedDns.lookup("www.google.com") assertThat(result).containsExactly(address("157.240.1.18")) - recordedRequest = server.takeRequest(0, TimeUnit.MILLISECONDS)!! - assertThat(recordedRequest.method).isEqualTo("POST") - assertThat(recordedRequest.url.encodedQuery) + val (httpsRequest2, _) = dnsOverHttpsServer.takeRequest() + assertThat(httpsRequest2.method).isEqualTo("POST") + assertThat(httpsRequest2.url.encodedQuery) .isEqualTo("ct") assertThat(cacheEvents()).containsExactly(CacheMiss::class) @@ -267,41 +286,35 @@ class DnsOverHttpsTest { val cache = Cache(File("./target/DnsOverHttpsTest.cache"), 100 * 1024L) val cachedClient = bootstrapClient.newBuilder().cache(cache).build() val cachedDns = buildLocalhost(cachedClient, false) - server.enqueue( - dnsResponse( - "0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c000500010" + - "0000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010001000" + - "0003b00049df00112", - ).newBuilder() - .setHeader("cache-control", "max-age=1") - .build(), - ) + 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"), + ), + ) var result = cachedDns.lookup("google.com") assertThat(result).containsExactly(address("157.240.1.18")) - var recordedRequest = server.takeRequest(0, TimeUnit.SECONDS) - assertThat(recordedRequest!!.method).isEqualTo("GET") - assertThat(recordedRequest.url.encodedQuery).isEqualTo( - "ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ", - ) + val (httpsRequest1, dnsRequest1) = dnsOverHttpsServer.takeRequest() + assertThat(httpsRequest1.method).isEqualTo("GET") + assertThat(dnsRequest1) + .isEqualTo(queryRequest("google.com", TYPE_A)) assertThat(cacheEvents()).containsExactly(CacheMiss::class) Thread.sleep(2000) - server.enqueue( - dnsResponse( - "0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c000500010" + - "0000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010001000" + - "0003b00049df00112", - ).newBuilder() - .setHeader("cache-control", "max-age=1") - .build(), - ) result = cachedDns.lookup("google.com") assertThat(result).isEqualTo(listOf(address("157.240.1.18"))) - recordedRequest = server.takeRequest(0, TimeUnit.SECONDS) - assertThat(recordedRequest!!.method).isEqualTo("GET") - assertThat(recordedRequest.url.encodedQuery) - .isEqualTo("ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ") + val (httpsRequest2, dnsRequest2) = dnsOverHttpsServer.takeRequest() + assertThat(httpsRequest2.method).isEqualTo("GET") + assertThat(dnsRequest2) + .isEqualTo(queryRequest("google.com", TYPE_A)) assertThat(cacheEvents()).containsExactly(CacheMiss::class) } @@ -312,14 +325,6 @@ class DnsOverHttpsTest { .filter { "Cache" in it.simpleName!! } .also { eventRecorder.clearAllEvents() } - private fun dnsResponse(s: String): MockResponse = - MockResponse - .Builder() - .body(Buffer().write(s.decodeHex())) - .addHeader("content-type", "application/dns-message") - .addHeader("content-length", s.length / 2) - .build() - private fun buildLocalhost( bootstrapClient: OkHttpClient, includeIPv6: Boolean, @@ -336,6 +341,31 @@ class DnsOverHttpsTest { .build() } + private fun overrideResponse(body: String): MockResponse = + MockResponse + .Builder() + .body(Buffer().write(body.decodeHex())) + .addHeader("content-type", "application/dns-message") + .addHeader("content-length", body.length / 2) + .build() + + private fun queryRequest( + host: String, + type: Int, + ): DnsMessage = + DnsMessage( + id = 0, + flags = 256, + questions = + listOf( + Question( + name = host, + type = type, + `class` = CLASS_IN, + ), + ), + ) + companion object { private fun address(host: String) = InetAddress.getByName(host) } diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/-UtilJvm.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/-UtilJvm.kt index ff6805cea202..80197be08184 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/-UtilJvm.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/-UtilJvm.kt @@ -370,7 +370,8 @@ internal val okHttpName: String = * * Returns the previous value. This will always be the replaced value if the value was updated. */ -internal tailrec fun AtomicReference.testAndSet( +@OkHttpInternalApi +tailrec fun AtomicReference.testAndSet( newValue: T, condition: (T) -> Boolean, ): T { diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/OkHttpInternalApi.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/OkHttpInternalApi.kt new file mode 100644 index 000000000000..6f226bee1a05 --- /dev/null +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/OkHttpInternalApi.kt @@ -0,0 +1,27 @@ +/* + * 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.internal + +import kotlin.RequiresOptIn.Level.ERROR +import kotlin.annotation.AnnotationRetention.BINARY +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.PROPERTY + +@RequiresOptIn(level = ERROR, message = "Internal APIs for OkHttp's own modules") +@Retention(BINARY) +@Target(CLASS, FUNCTION, PROPERTY) +annotation class OkHttpInternalApi diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/DnsAsDns2.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/DnsAsDns2.kt index 9cf10d2a2b78..878878e705e2 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/DnsAsDns2.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/DnsAsDns2.kt @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:OptIn(OkHttpInternalApi::class) + package okhttp3.internal.dns import java.io.IOException @@ -20,6 +22,7 @@ import java.net.UnknownHostException import java.util.concurrent.atomic.AtomicReference import okhttp3.Dns import okhttp3.Dns2 +import okhttp3.internal.OkHttpInternalApi import okhttp3.internal.concurrent.Task import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.testAndSet From eb1395598a4e958b8b50866a3d44df90edae671a Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Wed, 15 Jul 2026 16:20:42 -0400 Subject: [PATCH 2/2] Fixup internal API --- okhttp/src/jvmTest/kotlin/okhttp3/internal/TestAndSetTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/okhttp/src/jvmTest/kotlin/okhttp3/internal/TestAndSetTest.kt b/okhttp/src/jvmTest/kotlin/okhttp3/internal/TestAndSetTest.kt index e0b4628a8028..0a82865436cd 100644 --- a/okhttp/src/jvmTest/kotlin/okhttp3/internal/TestAndSetTest.kt +++ b/okhttp/src/jvmTest/kotlin/okhttp3/internal/TestAndSetTest.kt @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:OptIn(OkHttpInternalApi::class) + package okhttp3.internal import assertk.assertThat