From 3a66395756ba3b599ae3e54d9c5250f576827e79 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Sat, 25 Jul 2026 13:46:10 -0400 Subject: [PATCH 1/2] Cache empty results on the failure schedule. --- .../okhttp3/internal/dns/RealDnsCache.kt | 7 ++- .../internal/dns/StateMachineDnsCallTest.kt | 61 +++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/RealDnsCache.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/RealDnsCache.kt index eb95b73a5cf3..99ad1885ec52 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/RealDnsCache.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/RealDnsCache.kt @@ -204,10 +204,11 @@ class RealDnsCache( while (true) { val previous = entry.state.get() val sentAt = previous.inFlightCall!!.sentAt - val timeToLive = - (dnsResponse.answers.minOfOrNull { it.timeToLive } ?: 0) - .seconds + val timeToLive = when { + dnsResponse.answers.isEmpty() -> failureTimeToLive + else -> dnsResponse.answers.minOf { it.timeToLive }.seconds .coerceIn(minimumTimeToLive, maximumTimeToLive) + } val revalidateDelay = (timeToLive - revalidateBeforeExpire).coerceAtLeast(0.seconds) val next = diff --git a/okhttp/src/commonTest/kotlin/okhttp3/internal/dns/StateMachineDnsCallTest.kt b/okhttp/src/commonTest/kotlin/okhttp3/internal/dns/StateMachineDnsCallTest.kt index 13d2f94bd31c..0871a2aef844 100644 --- a/okhttp/src/commonTest/kotlin/okhttp3/internal/dns/StateMachineDnsCallTest.kt +++ b/okhttp/src/commonTest/kotlin/okhttp3/internal/dns/StateMachineDnsCallTest.kt @@ -738,6 +738,67 @@ class StateMachineDnsCallTest { assertThat(cache.hitCount).isEqualTo(0) } + @Test + fun `empty result expires on the same schedule as failure`() = + testStateMachineDnsCall { + val call0 = + newCall( + request = Dns.Request(hostname = "lysine.dev"), + caching = true, + includeIPv6 = false, + includeServiceMetadata = false, + ) + call0.enqueue() + queryFactory.respondToQuery( + hostname = "lysine.dev", + type = TYPE_A, + addresses = listOf(), + ) + call0.takeOnRecordsIpAddresses( + last = true, + addresses = listOf(), + ) + + // The empty result is still cached. + val call1 = + newCall( + request = Dns.Request(hostname = "lysine.dev"), + caching = true, + includeIPv6 = false, + includeServiceMetadata = false, + ) + call1.enqueue() + call1.takeOnRecordsIpAddresses( + last = true, + addresses = listOf(), + ) + + // The empty result expires after 5 seconds. + sleep(5.seconds) + val call2 = + newCall( + request = Dns.Request(hostname = "lysine.dev"), + caching = true, + includeIPv6 = false, + includeServiceMetadata = false, + ) + call2.enqueue() + queryFactory.respondToQuery( + hostname = "lysine.dev", + type = TYPE_A, + addresses = blueIpv4s, + ) + call2.takeOnRecordsIpAddresses( + last = true, + addresses = blueIpv4s, + ) + + assertThat(cache.size).isEqualTo(1) + assertThat(cache.requestCount).isEqualTo(3) + assertThat(cache.networkCount).isEqualTo(2) + assertThat(cache.hitCount).isEqualTo(1) + } + @Test fun `failure is revalidated`() = testStateMachineDnsCall { From 48ff0962a31f07440241650d3a6d9a8775942f05 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Sat, 25 Jul 2026 17:56:37 -0400 Subject: [PATCH 2/2] Spotless --- .../okhttp3/internal/dns/RealDnsCache.kt | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/RealDnsCache.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/RealDnsCache.kt index 99ad1885ec52..bbf46d1abe66 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/RealDnsCache.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/dns/RealDnsCache.kt @@ -204,11 +204,19 @@ class RealDnsCache( while (true) { val previous = entry.state.get() val sentAt = previous.inFlightCall!!.sentAt - val timeToLive = when { - dnsResponse.answers.isEmpty() -> failureTimeToLive - else -> dnsResponse.answers.minOf { it.timeToLive }.seconds - .coerceIn(minimumTimeToLive, maximumTimeToLive) - } + val timeToLive = + when { + dnsResponse.answers.isEmpty() -> { + failureTimeToLive + } + + else -> { + dnsResponse.answers + .minOf { it.timeToLive } + .seconds + .coerceIn(minimumTimeToLive, maximumTimeToLive) + } + } val revalidateDelay = (timeToLive - revalidateBeforeExpire).coerceAtLeast(0.seconds) val next =