Skip to content

Commit

Permalink
Use CacheControlInfo to pay attention to the Cache-Control http respo…
Browse files Browse the repository at this point in the history
…nse header and drop requests that are made too soon. We need to be nice to servers.
  • Loading branch information
brentsimmons committed Dec 1, 2024
1 parent 80c78b6 commit e57e3c9
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 3 deletions.
63 changes: 63 additions & 0 deletions RSWeb/Sources/RSWeb/CacheControlInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//
// CacheControl.swift
// RSWeb
//
// Created by Brent Simmons on 11/30/24.
//

import Foundation

/// Basic Cache-Control handling — just the part we need,
/// which is to know when we got the response (dateCreated)
/// and when we can ask again (dateExpired).
public struct CacheControlInfo: Codable, Equatable {

let dateCreated: Date
let maxAge: TimeInterval

var dateExpired: Date {
dateCreated + maxAge
}

public init?(urlResponse: HTTPURLResponse) {
guard let cacheControlValue = urlResponse.valueForHTTPHeaderField(HTTPResponseHeader.cacheControl) else {
return nil
}
self.init(value: cacheControlValue)
}

/// Returns nil if there’s no max-age or it’s < 1.
public init?(value: String) {

guard let maxAge = Self.parseMaxAge(value) else {
return nil
}

let d = Date()
self.dateCreated = d
self.maxAge = maxAge
}
}

private extension CacheControlInfo {

static let maxAgePrefix = "max-age="
static let maxAgePrefixCount = maxAgePrefix.count

static func parseMaxAge(_ s: String) -> TimeInterval? {

let components = s.components(separatedBy: ",")
let trimmedComponents = components.map { $0.trimmingCharacters(in: .whitespaces) }

for component in trimmedComponents {
if component.hasPrefix(Self.maxAgePrefix) {
let maxAgeStringValue = component.dropFirst(maxAgePrefixCount)
if let timeInterval = TimeInterval(maxAgeStringValue), timeInterval > 0 {
return timeInterval
}
}
}

return nil
}
}
35 changes: 32 additions & 3 deletions RSWeb/Sources/RSWeb/DownloadSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import Foundation
import os

// Create a DownloadSessionDelegate, then create a DownloadSession.
// To download things: call download with a set of URLs. DownloadSession will call the various delegate methods.
Expand All @@ -31,6 +32,7 @@ public protocol DownloadSessionDelegate {
private let delegate: DownloadSessionDelegate
private var redirectCache = [URL: URL]()
private var queue = [URL]()
private var cacheControlResponses = [URL: CacheControlInfo]()

// 429 Too Many Requests responses
private var retryAfterMessages = [String: HTTPResponse429]()
Expand Down Expand Up @@ -128,9 +130,10 @@ extension DownloadSession: URLSessionDataDelegate {

tasksInProgress.insert(dataTask)
tasksPending.remove(dataTask)

if let info = infoForTask(dataTask) {
info.urlResponse = response

let taskInfo = infoForTask(dataTask)
if let taskInfo {
taskInfo.urlResponse = response
}

if !response.statusIsOK {
Expand All @@ -149,6 +152,15 @@ extension DownloadSession: URLSessionDataDelegate {
return
}

if let httpURLResponse = response as? HTTPURLResponse, let cacheControlInfo = CacheControlInfo(urlResponse: httpURLResponse) {
if let url = taskInfo?.url {
cacheControlResponses[url] = cacheControlInfo
if let actualURL = response.url, actualURL != url {
cacheControlResponses[actualURL] = cacheControlInfo
}
}
}

addDataTaskFromQueueIfNecessary()
completionHandler(.allow)
}
Expand Down Expand Up @@ -182,9 +194,15 @@ private extension DownloadSession {
let urlToUse = cachedRedirect(for: url) ?? url

if requestShouldBeDroppedDueToActive429(urlToUse) {
os_log(.debug, "Dropping request for previous 429: \(urlToUse)")
return
}
if requestShouldBeDroppedDueToPrevious400(urlToUse) {
os_log(.debug, "Dropping request for previous 400-499: \(urlToUse)")
return
}
if requestShouldBeDroppedDueToCacheControl(urlToUse) {
os_log(.debug, "Dropping request for Cache-Control reasons: \(urlToUse)")
return
}

Expand Down Expand Up @@ -380,6 +398,17 @@ private extension DownloadSession {

return false
}

// MARK: - Cache-Control responses

func requestShouldBeDroppedDueToCacheControl(_ url: URL) -> Bool {

guard let cacheControlInfo = cacheControlResponses[url] else {
return false
}

return cacheControlInfo.dateExpired > Date()
}
}

extension URLSessionTask {
Expand Down

0 comments on commit e57e3c9

Please sign in to comment.