Skip to content

Commit 6a20523

Browse files
authored
Evaluate cache strategy before enforcing failure response code (#3401)
When a failure response (e.g. 404) was cached to disk via CACHEABLE_STATUS_CODES, NetworkFetcher threw HttpException before invoking CacheStrategy.read(). This prevented cache-aware strategies (e.g. CacheControlCacheStrategy) from ever seeing the cached response, so a cached failure could never expire via cache-control headers. Move the failure code check until after CacheStrategy.read() returns a response. When the strategy opts to reuse the cached response, the check still fires and preserves existing behavior for the default strategy. When the strategy returns a fresh request instead, the fetcher now proceeds to the network as expected. Resolves #3391
1 parent 69b8383 commit 6a20523

2 files changed

Lines changed: 75 additions & 2 deletions

File tree

coil-network-core/src/commonMain/kotlin/coil3/network/NetworkFetcher.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,13 @@ class NetworkFetcher(
8181
// Return the image from the disk cache if the cache strategy agrees.
8282
cacheResponse = snapshot.toNetworkResponseOrNull()
8383
if (cacheResponse != null) {
84-
throwIfFailureResponseCode(cacheResponse)
85-
8684
readResult = cacheStrategy.value.read(cacheResponse, newRequest(), options)
8785
if (readResult.response != null) {
86+
// Only enforce the failure code check when the strategy opts to use the
87+
// cached response. This lets cache-aware strategies (e.g. cache-control)
88+
// issue a fresh request when a cached failure (e.g. 404) has expired.
89+
throwIfFailureResponseCode(readResult.response)
90+
8891
return SourceFetchResult(
8992
source = snapshot.toImageSource(),
9093
mimeType = getMimeType(url, readResult.response.headers[CONTENT_TYPE]),

coil-network-core/src/commonTest/kotlin/coil3/network/NetworkFetcherTest.kt

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,76 @@ class NetworkFetcherTest : RobolectricTest() {
204204
fileSystem.checkNoOpenFiles()
205205
}
206206

207+
@Test
208+
fun cached404IsRefreshedWhenCacheStrategyRequestsNewRequest() = runTestAsync {
209+
val url = "https://example.com/cached-404-refresh.jpg"
210+
val expectedSize = 1_000
211+
212+
val fileSystem = FakeFileSystem()
213+
val diskCache = DiskCache.Builder()
214+
.directory(fileSystem.workingDirectory)
215+
.fileSystem(fileSystem)
216+
.maxSizeBytes(Long.MAX_VALUE)
217+
.build()
218+
219+
// Pre-populate the disk cache with a cached 404 response.
220+
val editor = diskCache.openEditor(url)!!
221+
fileSystem.write(editor.metadata) {
222+
CacheNetworkResponse.writeTo(NetworkResponse(code = 404), this)
223+
}
224+
fileSystem.write(editor.data) {
225+
write(ByteArray(32))
226+
}
227+
editor.commit()
228+
229+
val networkClient = FakeNetworkClient(
230+
respond = {
231+
NetworkResponse(
232+
body = NetworkResponseBody(
233+
source = Buffer().apply { write(ByteArray(expectedSize)) },
234+
),
235+
)
236+
},
237+
)
238+
239+
// Custom strategy that always asks for a fresh network request, simulating an expired
240+
// cached response (similar to what `CacheControlCacheStrategy` does when a cached
241+
// failure has become stale).
242+
val refreshingStrategy = object : CacheStrategy {
243+
override suspend fun read(
244+
cacheResponse: NetworkResponse,
245+
networkRequest: NetworkRequest,
246+
options: Options,
247+
) = CacheStrategy.ReadResult(networkRequest)
248+
249+
override suspend fun write(
250+
cacheResponse: NetworkResponse?,
251+
networkRequest: NetworkRequest,
252+
networkResponse: NetworkResponse,
253+
options: Options,
254+
) = CacheStrategy.WriteResult(networkResponse)
255+
}
256+
257+
val fetcher = NetworkFetcher(
258+
url = url,
259+
options = Options(context),
260+
networkClient = lazyOf(networkClient),
261+
diskCache = lazyOf(diskCache),
262+
cacheStrategy = lazyOf(refreshingStrategy),
263+
connectivityChecker = lazyOf(ConnectivityChecker.ONLINE),
264+
concurrentRequestStrategy = lazyOf(ConcurrentRequestStrategy.UNCOORDINATED),
265+
)
266+
267+
// The cached 404 should no longer short-circuit with `HttpException` when the
268+
// cache strategy opts to refresh; a network request should be issued instead.
269+
val result = fetcher.fetch()
270+
assertIs<SourceFetchResult>(result)
271+
assertEquals(1, networkClient.requests.size)
272+
273+
diskCache.shutdown()
274+
fileSystem.checkNoOpenFiles()
275+
}
276+
207277
class FakeNetworkClient(
208278
private val respond: suspend (NetworkRequest) -> NetworkResponse,
209279
) : NetworkClient {

0 commit comments

Comments
 (0)