253 lines · 9.2 KB · Swift
RawDownload
import Foundation#if canImport(FoundationNetworking)import FoundationNetworking#endif public protocol ProviderHTTPTransport: Sendable {    func data(for request: URLRequest) async throws -> (Data, URLResponse)} #if !os(Linux)extension URLSession: ProviderHTTPTransport {}#endif extension URLSession {    public func response(for request: URLRequest) async throws -> ProviderHTTPResponse {        let (data, response) = try await self.data(for: request)        guard let httpResponse = response as? HTTPURLResponse else {            throw URLError(.badServerResponse)        }        return ProviderHTTPResponse(data: data, response: httpResponse)    }} public struct ProviderHTTPResponse: Sendable {    public let data: Data    public let response: HTTPURLResponse     public init(data: Data, response: HTTPURLResponse) {        self.data = data        self.response = response    }     public var statusCode: Int {        self.response.statusCode    }} public struct ProviderHTTPRetryPolicy: Sendable {    public let maxRetries: Int    public let retryableStatusCodes: Set<Int>    public let retryableURLErrorCodes: Set<URLError.Code>    public let retryableMethods: Set<String>    public let baseDelaySeconds: TimeInterval    public let maxDelaySeconds: TimeInterval     public init(        maxRetries: Int,        retryableStatusCodes: Set<Int> = [408, 429, 500, 502, 503, 504],        retryableURLErrorCodes: Set<URLError.Code> = [            .timedOut,            .networkConnectionLost,            .cannotConnectToHost,            .cannotFindHost,            .dnsLookupFailed,        ],        retryableMethods: Set<String> = ["GET", "HEAD", "OPTIONS"],        baseDelaySeconds: TimeInterval = 1,        maxDelaySeconds: TimeInterval = 10)    {        self.maxRetries = max(0, maxRetries)        self.retryableStatusCodes = retryableStatusCodes        self.retryableURLErrorCodes = retryableURLErrorCodes        self.retryableMethods = retryableMethods        self.baseDelaySeconds = max(0, baseDelaySeconds)        self.maxDelaySeconds = max(0, maxDelaySeconds)    }     public static let disabled = ProviderHTTPRetryPolicy(        maxRetries: 0,        retryableStatusCodes: [],        retryableURLErrorCodes: [],        baseDelaySeconds: 0,        maxDelaySeconds: 0)     public static let transientIdempotent = ProviderHTTPRetryPolicy(maxRetries: 1)     func shouldRetry(request: URLRequest, attempt: Int, statusCode: Int) -> Bool {        self.canRetry(request: request, attempt: attempt)            && self.retryableStatusCodes.contains(statusCode)    }     func shouldRetry(request: URLRequest, attempt: Int, error: Error) -> Bool {        guard self.canRetry(request: request, attempt: attempt) else { return false }        guard let urlError = error as? URLError else { return false }        return self.retryableURLErrorCodes.contains(urlError.code)    }     func delaySeconds(attempt: Int, response: HTTPURLResponse?) -> TimeInterval {        if let retryAfter = response?.value(forHTTPHeaderField: "Retry-After"),           let seconds = TimeInterval(retryAfter.trimmingCharacters(in: .whitespacesAndNewlines)),           seconds >= 0        {            return min(seconds, self.maxDelaySeconds)        }         guard self.baseDelaySeconds > 0 else { return 0 }        let multiplier = pow(2, Double(max(0, attempt)))        return min(self.baseDelaySeconds * multiplier, self.maxDelaySeconds)    }     private func canRetry(request: URLRequest, attempt: Int) -> Bool {        guard attempt < self.maxRetries else { return false }        let method = request.httpMethod?.uppercased() ?? "GET"        return self.retryableMethods.contains(method)    }} public struct ProviderHTTPTransportHandler: ProviderHTTPTransport {    private let handler: @Sendable (URLRequest) async throws -> (Data, URLResponse)     public init(_ handler: @escaping @Sendable (URLRequest) async throws -> (Data, URLResponse)) {        self.handler = handler    }     public func data(for request: URLRequest) async throws -> (Data, URLResponse) {        try await self.handler(request)    }} extension ProviderHTTPTransport {    public func response(for request: URLRequest) async throws -> ProviderHTTPResponse {        try await self.response(for: request, retryPolicy: .disabled)    }     public func response(        for request: URLRequest,        retryPolicy: ProviderHTTPRetryPolicy) async throws -> ProviderHTTPResponse    {        var attempt = 0         while true {            do {                let (data, response) = try await self.data(for: request)                guard let httpResponse = response as? HTTPURLResponse else {                    throw URLError(.badServerResponse)                }                let providerResponse = ProviderHTTPResponse(data: data, response: httpResponse)                guard retryPolicy.shouldRetry(                    request: request,                    attempt: attempt,                    statusCode: providerResponse.statusCode)                else {                    return providerResponse                }                try await Self.sleepBeforeRetry(policy: retryPolicy, attempt: attempt, response: httpResponse)                attempt += 1            } catch {                guard retryPolicy.shouldRetry(request: request, attempt: attempt, error: error) else {                    throw error                }                try await Self.sleepBeforeRetry(policy: retryPolicy, attempt: attempt, response: nil)                attempt += 1            }        }    }     private static func sleepBeforeRetry(        policy: ProviderHTTPRetryPolicy,        attempt: Int,        response: HTTPURLResponse?) async throws    {        let delay = policy.delaySeconds(attempt: attempt, response: response)        guard delay > 0 else { return }        try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))    }} public final class ProviderHTTPClient: ProviderHTTPTransport, @unchecked Sendable {    public static let shared = ProviderHTTPClient(session: ProviderHTTPClient.sharedSession())     private let session: URLSession     public init(session: URLSession? = nil) {        self.session = session ?? Self.redirectGuardedSession()    }     static func defaultConfiguration() -> URLSessionConfiguration {        let configuration = URLSessionConfiguration.default        configuration.timeoutIntervalForRequest = 30        configuration.timeoutIntervalForResource = 90        #if !os(Linux)        configuration.waitsForConnectivity = false        #endif        return configuration    }     private static func sharedSession() -> URLSession {        if self.isRunningTests {            // XCTest URLProtocol.registerClass stubs only intercept URLSession.shared on macOS.            return .shared        }        return self.redirectGuardedSession()    }     static func redirectGuardedSession(        configuration: URLSessionConfiguration = ProviderHTTPClient.defaultConfiguration()) -> URLSession    {        URLSession(            configuration: configuration,            delegate: ProviderHTTPRedirectGuardDelegate(),            delegateQueue: nil)    }     private static var isRunningTests: Bool {        let environment = ProcessInfo.processInfo.environment        if environment["XCTestConfigurationFilePath"] != nil || environment["XCTestBundlePath"] != nil {            return true        }        if ProcessInfo.processInfo.processName.lowercased().contains("xctest") {            return true        }        return CommandLine.arguments.contains { $0.lowercased().contains(".xctest") }    }     public func data(for request: URLRequest) async throws -> (Data, URLResponse) {        try await self.session.data(for: request)    }} final class ProviderHTTPRedirectGuardDelegate: NSObject, URLSessionTaskDelegate, @unchecked Sendable {    func urlSession(        _: URLSession,        task: URLSessionTask,        willPerformHTTPRedirection _: HTTPURLResponse,        newRequest request: URLRequest,        completionHandler: @escaping @Sendable (URLRequest?) -> Void)    {        completionHandler(Self.guardedRedirectRequest(originalURL: task.originalRequest?.url, redirectRequest: request))    }     static func guardedRedirectRequest(originalURL: URL?, redirectRequest request: URLRequest) -> URLRequest? {        guard let originalURL, let redirectedURL = request.url else { return nil }        guard originalURL.scheme?.caseInsensitiveCompare("https") == .orderedSame else { return nil }        guard redirectedURL.scheme?.caseInsensitiveCompare("https") == .orderedSame else { return nil }        guard self.isSameOrigin(originalURL, redirectedURL) else { return nil }        return request    }     private static func isSameOrigin(_ lhs: URL, _ rhs: URL) -> Bool {        lhs.scheme?.lowercased() == rhs.scheme?.lowercased()            && lhs.host?.lowercased() == rhs.host?.lowercased()            && self.normalizedPort(lhs) == self.normalizedPort(rhs)    }     private static func normalizedPort(_ url: URL) -> Int? {        if let port = url.port { return port }        switch url.scheme?.lowercased() {        case "http": return 80        case "https": return 443        default: return nil        }    }}