Repository: ktorio/ktor
File: ktor-client/ktor-client-core/common/src/io/ktor/client/plugins/cache/HttpCache.kt (Kotlin)

Code before:

        override fun prepare(block: Config.() -> Unit): HttpCache {
            val config = Config().apply(block)

            with(config) {
                @Suppress("DEPRECATION_ERROR")
                return HttpCache(
                    publicStorage = publicStorage,
                    privateStorage = privateStorage,
                    publicStorageNew = publicStorageNew,
                    privateStorageNew = privateStorageNew,
                    useOldStorage = useOldStorage,
                    isSharedClient = isShared
                )
            }
        }

        @OptIn(InternalAPI::class)
        override fun install(plugin: HttpCache, scope: HttpClient) {
            val CachePhase = PipelinePhase("Cache")
            scope.sendPipeline.insertPhaseAfter(HttpSendPipeline.State, CachePhase)

            scope.sendPipeline.intercept(CachePhase) { content ->
                if (content !is OutgoingContent.NoContent) return@intercept
                if (context.method != HttpMethod.Get || !context.url.protocol.canStore()) return@intercept
                if (context.headers.contains(HttpHeaders.Authorization)) return@intercept

                if (plugin.useOldStorage) {
                    interceptSendLegacy(plugin, content, scope)
                    return@intercept
                }

                val cache = plugin.findResponse(context, content)
                if (cache == null) {
                    LOGGER.trace("No cached response for ${context.url} found")
                    val header = parseHeaderValue(context.headers[HttpHeaders.CacheControl])
                    if (CacheControl.ONLY_IF_CACHED in header) {
                        LOGGER.trace("No cache found and \"only-if-cached\" set for ${context.url}")
                        proceedWithMissingCache(scope)
                    }
                    return@intercept
                }
                val validateStatus = shouldValidate(cache.expires, cache.headers, context)

                if (validateStatus == ValidateStatus.ShouldNotValidate) {
                    val cachedCall = cache
                        .createResponse(scope, RequestForCache(context.build()), context.executionContext)
                        .call
                    proceedWithCache(scope, cachedCall)
                    return@intercept
                }

Review comment (security):
Could this instead be configurable this behaviour as the default?  Assuming the authentication is still enforced for cached responses, it would be useful to cache these responses as well.

Code after:
         *
         * Consider using [CacheStorage.Disabled] if the client is used as intermediate.
         */
        public fun privateStorage(storage: CacheStorage) {
            privateStorageNew = storage
        }
    }

    public companion object : HttpClientPlugin<Config, HttpCache> {
        override val key: AttributeKey<HttpCache> = AttributeKey("HttpCache")

        public val HttpResponseFromCache: EventDefinition<HttpResponse> = EventDefinition()

        override fun prepare(block: Config.() -> Unit): HttpCache {
            val config = Config().apply(block)

            with(config) {
                @Suppress("DEPRECATION_ERROR")
                return HttpCache(
                    publicStorage = publicStorage,
                    privateStorage = privateStorage,
                    publicStorageNew = publicStorageNew,
                    privateStorageNew = privateStorageNew,
                    useOldStorage = useOldStorage,
                    isSharedClient = isShared,
                    cacheRequestWithAuth = cacheRequestWithAuth
                )
            }
        }

        @OptIn(InternalAPI::class)
        override fun install(plugin: HttpCache, scope: HttpClient) {
            val CachePhase = PipelinePhase("Cache")
            scope.sendPipeline.insertPhaseAfter(HttpSendPipeline.State, CachePhase)

            scope.sendPipeline.intercept(CachePhase) { content ->
                if (content !is OutgoingContent.NoContent) return@intercept
                if (context.method != HttpMethod.Get || !context.url.protocol.canStore()) return@intercept

                if (!plugin.cacheRequestWithAuth && context.headers.contains(HttpHeaders.Authorization)) {
                    return@intercept
                }

                if (plugin.useOldStorage) {
                    interceptSendLegacy(plugin, content, scope)
                    return@intercept
                }

                val cache = plugin.findResponse(context, content)
                if (cache == null) {
                    LOGGER.trace("No cached response for ${context.url} found")