commonMain.io.ktor.client.plugins.cache.HttpCacheEntry.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ktor-client-core-jvm Show documentation
Show all versions of ktor-client-core-jvm Show documentation
Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.
/*
* Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.client.plugins.cache
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.util.*
import io.ktor.util.date.*
import io.ktor.utils.io.core.*
import kotlin.collections.*
@OptIn(InternalAPI::class)
internal suspend fun HttpCacheEntry(response: HttpResponse): HttpCacheEntry {
val body = response.content.readRemaining().readBytes()
response.complete()
return HttpCacheEntry(response.cacheExpires(), response.varyKeys(), response, body)
}
/**
* Client single response cache with [expires] and [varyKeys].
*/
public class HttpCacheEntry internal constructor(
public val expires: GMTDate,
public val varyKeys: Map,
public val response: HttpResponse,
public val body: ByteArray
) {
internal val responseHeaders: Headers = Headers.build {
appendAll(response.headers)
}
internal fun produceResponse(): HttpResponse {
val currentClient = response.call.client
val call = SavedHttpCall(currentClient, response.call.request, response, body)
return call.response
}
override fun equals(other: Any?): Boolean {
if (other == null || other !is HttpCacheEntry) return false
if (other === this) return true
return varyKeys == other.varyKeys
}
override fun hashCode(): Int {
return varyKeys.hashCode()
}
}
internal fun HttpResponse.varyKeys(): Map {
val validationKeys = vary() ?: return emptyMap()
val result = mutableMapOf()
val requestHeaders = call.request.headers
for (key in validationKeys) {
result[key] = requestHeaders[key] ?: ""
}
return result
}
internal fun HttpResponse.cacheExpires(fallback: () -> GMTDate = { GMTDate() }): GMTDate {
val cacheControl = cacheControl()
val isPrivate = CacheControl.PRIVATE in cacheControl
val maxAgeKey = if (isPrivate) "s-max-age" else "max-age"
val maxAge = cacheControl.firstOrNull { it.value.startsWith(maxAgeKey) }
?.value?.split("=")
?.get(1)?.toInt()
if (maxAge != null) {
return call.response.requestTime + maxAge * 1000L
}
val expires = headers[HttpHeaders.Expires]
return expires?.let {
// Handle "0" case faster
if (it == "0" || it.isBlank()) return fallback()
return try {
it.fromHttpToGmtDate()
} catch (e: Throwable) {
fallback()
}
} ?: fallback()
}
internal fun shouldValidate(
cacheExpires: GMTDate,
responseHeaders: Headers,
request: HttpRequestBuilder
): ValidateStatus {
val requestHeaders = request.headers
val responseCacheControl = parseHeaderValue(responseHeaders[HttpHeaders.CacheControl])
val requestCacheControl = parseHeaderValue(requestHeaders[HttpHeaders.CacheControl])
if (CacheControl.NO_CACHE in requestCacheControl) {
LOGGER.trace("\"no-cache\" is set for ${request.url}, should validate cached response")
return ValidateStatus.ShouldValidate
}
val requestMaxAge = requestCacheControl.firstOrNull { it.value.startsWith("max-age=") }
?.value?.split("=")
?.get(1)?.let { it.toIntOrNull() ?: 0 }
if (requestMaxAge == 0) {
LOGGER.trace("\"max-age\" is not set for ${request.url}, should validate cached response")
return ValidateStatus.ShouldValidate
}
val validMillis = cacheExpires.timestamp - getTimeMillis()
if (CacheControl.NO_CACHE in responseCacheControl) {
LOGGER.trace("\"no-cache\" is set for ${request.url}, should validate cached response")
return ValidateStatus.ShouldValidate
}
if (validMillis > 0) {
LOGGER.trace("Cached response is valid for ${request.url}, should not validate")
return ValidateStatus.ShouldNotValidate
}
if (CacheControl.MUST_REVALIDATE in responseCacheControl) {
LOGGER.trace("\"must-revalidate\" is set for ${request.url}, should validate cached response")
return ValidateStatus.ShouldValidate
}
val maxStale = requestCacheControl.firstOrNull { it.value.startsWith("max-stale=") }
?.value?.substring("max-stale=".length)
?.toIntOrNull() ?: 0
val maxStaleMillis = maxStale * 1000L
if (validMillis + maxStaleMillis > 0) {
LOGGER.trace("Cached response is stale for ${request.url} but less than max-stale, should warn")
return ValidateStatus.ShouldWarn
}
LOGGER.trace("Cached response is stale for ${request.url}, should validate cached response")
return ValidateStatus.ShouldValidate
}
internal enum class ValidateStatus {
ShouldValidate, ShouldNotValidate, ShouldWarn
}