
commonMain.io.ktor.client.plugins.cache.HttpCacheLegacy.kt Maven / Gradle / Ivy
/*
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
@file:Suppress("DEPRECATION")
package io.ktor.client.plugins.cache
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.cache.HttpCache.Companion.proceedWithCache
import io.ktor.client.plugins.cache.HttpCache.Companion.proceedWithMissingCache
import io.ktor.client.plugins.cache.storage.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.util.*
import io.ktor.util.pipeline.*
internal suspend fun PipelineContext.interceptSendLegacy(
plugin: HttpCache,
content: OutgoingContent,
scope: HttpClient
) {
val cache = plugin.findResponse(context, content)
if (cache == null) {
val header = parseHeaderValue(context.headers[HttpHeaders.CacheControl])
if (CacheControl.ONLY_IF_CACHED in header) {
proceedWithMissingCache(scope)
}
return
}
val cachedCall = cache.produceResponse().call
val validateStatus = shouldValidate(cache.expires, cache.response.headers, context)
if (validateStatus == ValidateStatus.ShouldNotValidate) {
proceedWithCache(scope, cachedCall)
return
}
if (validateStatus == ValidateStatus.ShouldWarn) {
proceedWithWarning(cachedCall, scope)
return
}
cache.responseHeaders[HttpHeaders.ETag]?.let { etag ->
context.header(HttpHeaders.IfNoneMatch, etag)
}
cache.responseHeaders[HttpHeaders.LastModified]?.let {
context.header(HttpHeaders.IfModifiedSince, it)
}
}
@OptIn(InternalAPI::class)
internal suspend fun PipelineContext.interceptReceiveLegacy(
response: HttpResponse,
plugin: HttpCache,
scope: HttpClient
) {
if (response.status.isSuccess()) {
val reusableResponse = plugin.cacheResponse(response)
proceedWith(reusableResponse)
return
}
if (response.status == HttpStatusCode.NotModified) {
response.complete()
val responseFromCache = plugin.findAndRefresh(response.call.request, response)
?: throw InvalidCacheStateException(response.call.request.url)
scope.monitor.raise(HttpCache.HttpResponseFromCache, responseFromCache)
proceedWith(responseFromCache)
}
}
@OptIn(InternalAPI::class)
private suspend fun PipelineContext.proceedWithWarning(
cachedCall: HttpClientCall,
scope: HttpClient
) {
val request = context.build()
val response = HttpResponseData(
statusCode = cachedCall.response.status,
requestTime = cachedCall.response.requestTime,
headers = Headers.build {
appendAll(cachedCall.response.headers)
append(HttpHeaders.Warning, "110")
},
version = cachedCall.response.version,
body = cachedCall.response.content,
callContext = cachedCall.response.coroutineContext
)
val call = HttpClientCall(scope, request, response)
finish()
scope.monitor.raise(HttpCache.HttpResponseFromCache, call.response)
proceedWith(call)
}
private suspend fun HttpCache.cacheResponse(response: HttpResponse): HttpResponse {
val request = response.call.request
val responseCacheControl: List = response.cacheControl()
val requestCacheControl: List = request.cacheControl()
val storage = if (CacheControl.PRIVATE in responseCacheControl) privateStorage else publicStorage
if (CacheControl.NO_STORE in responseCacheControl || CacheControl.NO_STORE in requestCacheControl) {
return response
}
val cacheEntry = storage.store(request.url, response, isSharedClient)
return cacheEntry.produceResponse()
}
private fun HttpCache.findAndRefresh(request: HttpRequest, response: HttpResponse): HttpResponse? {
val url = response.call.request.url
val cacheControl = response.cacheControl()
val storage = if (CacheControl.PRIVATE in cacheControl) privateStorage else publicStorage
val varyKeysFrom304 = response.varyKeys()
val cache = findResponse(storage, varyKeysFrom304, url, request) ?: return null
val newVaryKeys = varyKeysFrom304.ifEmpty { cache.varyKeys }
storage.store(url, HttpCacheEntry(response.cacheExpires(isSharedClient), newVaryKeys, cache.response, cache.body))
return cache.produceResponse()
}
private fun HttpCache.findResponse(
storage: HttpCacheStorage,
varyKeys: Map,
url: Url,
request: HttpRequest
): HttpCacheEntry? = when {
varyKeys.isNotEmpty() -> {
storage.find(url, varyKeys)
}
else -> {
val requestHeaders = mergedHeadersLookup(request.content, request.headers::get, request.headers::getAll)
storage.findByUrl(url)
.sortedByDescending { it.response.responseTime }
.firstOrNull { cachedResponse ->
cachedResponse.varyKeys.all { (key, value) -> requestHeaders(key) == value }
}
}
}
private fun HttpCache.findResponse(context: HttpRequestBuilder, content: OutgoingContent): HttpCacheEntry? {
val url = Url(context.url)
val lookup = mergedHeadersLookup(content, context.headers::get, context.headers::getAll)
val cachedResponses = privateStorage.findByUrl(url) + publicStorage.findByUrl(url)
for (item in cachedResponses) {
val varyKeys = item.varyKeys
if (varyKeys.isEmpty() || varyKeys.all { (key, value) -> lookup(key) == value }) {
return item
}
}
return null
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy