diff --git a/src/main/java/io/github/nstdio/http/ext/CacheEntryMetadata.java b/src/main/java/io/github/nstdio/http/ext/CacheEntryMetadata.java index d6d6426..a688cdc 100644 --- a/src/main/java/io/github/nstdio/http/ext/CacheEntryMetadata.java +++ b/src/main/java/io/github/nstdio/http/ext/CacheEntryMetadata.java @@ -208,6 +208,10 @@ long maxAge() { return maxAge; } + long staleFor() { + return age(MILLISECONDS) - maxAge; + } + long requestTime() { return requestTimeMs; } diff --git a/src/main/java/io/github/nstdio/http/ext/CachingInterceptor.java b/src/main/java/io/github/nstdio/http/ext/CachingInterceptor.java index f567dde..5b88ee4 100644 --- a/src/main/java/io/github/nstdio/http/ext/CachingInterceptor.java +++ b/src/main/java/io/github/nstdio/http/ext/CachingInterceptor.java @@ -20,6 +20,7 @@ import static io.github.nstdio.http.ext.Headers.HEADER_IF_MODIFIED_SINCE; import static io.github.nstdio.http.ext.Headers.HEADER_IF_NONE_MATCH; import static io.github.nstdio.http.ext.Responses.gatewayTimeoutResponse; +import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.stream.Collectors.toList; import static lombok.Lombok.sneakyThrow; @@ -210,9 +211,28 @@ private boolean isFresh(RequestContext ctx, Cache.CacheEntry entry) { private HttpResponse possiblyCached(RequestContext ctx, Cache.CacheEntry entry, HttpResponse r) { CacheEntryMetadata metadata = entry != null ? entry.metadata() : null; - if (metadata != null && r.statusCode() == 304) { - metadata.update(r.headers(), ctx.requestTimeLong(), ctx.responseTimeLong()); - return createCachedResponse(ctx, entry); + if (metadata != null) { + switch (r.statusCode()) { + case 304: { + metadata.update(r.headers(), ctx.requestTimeLong(), ctx.responseTimeLong()); + return createCachedResponse(ctx, entry); + } + case 500: + case 502: + case 503: + case 504: { + long staleIfError = Math.max( + ctx.cacheControl().staleIfError(MILLISECONDS), + metadata.responseCacheControl().staleIfError(MILLISECONDS) + ); + + if (staleIfError > metadata.staleFor()) { + return createCachedResponse(ctx, entry); + } + + break; + } + } } return r; diff --git a/src/test/java/io/github/nstdio/http/ext/ExtendedHttpClientContract.java b/src/test/java/io/github/nstdio/http/ext/ExtendedHttpClientContract.java index dc060e6..59d27b6 100644 --- a/src/test/java/io/github/nstdio/http/ext/ExtendedHttpClientContract.java +++ b/src/test/java/io/github/nstdio/http/ext/ExtendedHttpClientContract.java @@ -16,11 +16,13 @@ package io.github.nstdio.http.ext; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.created; import static com.github.tomakehurst.wiremock.client.WireMock.delete; import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.matching; import static com.github.tomakehurst.wiremock.client.WireMock.noContent; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.put; @@ -35,6 +37,7 @@ import static io.github.nstdio.http.ext.Headers.HEADER_CACHE_CONTROL; import static io.github.nstdio.http.ext.Headers.HEADER_DATE; import static io.github.nstdio.http.ext.Headers.HEADER_ETAG; +import static io.github.nstdio.http.ext.Headers.HEADER_IF_MODIFIED_SINCE; import static io.github.nstdio.http.ext.Headers.HEADER_IF_NONE_MATCH; import static io.github.nstdio.http.ext.Headers.HEADER_LAST_MODIFIED; import static io.github.nstdio.http.ext.Headers.toRFC1123; @@ -587,9 +590,39 @@ default void shouldWorkWithPathSubscriber(@TempDir Path tempDir) throws Exceptio Assertions.assertThat(r2Path).exists().hasContent(body); } + /** + * https://datatracker.ietf.org/doc/html/rfc5861#section-4 + */ @Test - @Disabled("https://datatracker.ietf.org/doc/html/rfc5861#section-4") - default void shouldRespectStaleIfError() { + default void shouldRespectStaleIfError() throws Exception { + //given + var clock = FixedRateTickClock.of(clock(), Duration.ofSeconds(1)); + var client = client(clock); + var bodyHandler = ofString(); + var urlPattern = urlEqualTo(path()); + + stubFor(get(urlPattern) + .willReturn(ok() + .withHeader(HEADER_CACHE_CONTROL, "max-age=1,stale-if-error=10") + .withBody("abc") + ) + ); + stubFor(get(urlPattern) + .withHeader(HEADER_IF_MODIFIED_SINCE, matching(".+")) + .willReturn(aResponse().withStatus(500)) + ); + + //when + var r1 = client.send(requestBuilder().build(), bodyHandler); + var r2 = await().until(() -> client.send(requestBuilder().header("Cache-Control", "stale-if-error=4").build(), bodyHandler), isCached()); + var r3 = await().until(() -> client.send(requestBuilder().header("Cache-Control", "stale-if-error=100").build(), bodyHandler), isCached()); + var r4 = client.send(requestBuilder().header("Cache-Control", "stale-if-error=1").build(), bodyHandler); + + //then + assertThat(r1).isNotCached().hasBody("abc"); + assertThat(r2).isCached().hasBody("abc"); + assertThat(r3).isCached().hasBody("abc"); + assertThat(r4).isNotCached().hasStatusCode(500); } @Test