2021-07-30 23:03:25 +02:00
|
|
|
//
|
|
|
|
|
// Copyright RevenueCat Inc. All Rights Reserved.
|
|
|
|
|
//
|
|
|
|
|
// Licensed under the MIT License (the "License");
|
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
|
//
|
|
|
|
|
// https://opensource.org/licenses/MIT
|
|
|
|
|
//
|
|
|
|
|
// HTTPClient.swift
|
|
|
|
|
//
|
|
|
|
|
// Created by César de la Vega on 7/22/21.
|
|
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
|
|
2022-08-31 13:30:38 -07:00
|
|
|
// swiftlint:disable file_length
|
|
|
|
|
|
2021-08-23 11:20:55 -07:00
|
|
|
class HTTPClient {
|
2021-07-30 23:03:25 +02:00
|
|
|
|
2023-02-22 11:30:20 -08:00
|
|
|
typealias RequestHeaders = HTTPRequest.Headers
|
|
|
|
|
typealias ResponseHeaders = HTTPResponse<HTTPEmptyResponseBody>.Headers
|
2023-06-30 12:24:53 -07:00
|
|
|
typealias Completion<Value: HTTPResponseBody> = (VerifiedHTTPResponse<Value>.Result) -> Void
|
2022-03-04 13:46:24 -08:00
|
|
|
|
2022-06-14 15:12:46 -07:00
|
|
|
let systemInfo: SystemInfo
|
|
|
|
|
let timeout: TimeInterval
|
|
|
|
|
let apiKey: String
|
|
|
|
|
let authHeaders: RequestHeaders
|
2022-05-12 15:43:10 -07:00
|
|
|
|
|
|
|
|
private let session: URLSession
|
2022-02-23 18:05:59 -05:00
|
|
|
private let state: Atomic<State> = .init(.initial)
|
2022-02-19 10:11:52 -07:00
|
|
|
private let eTagManager: ETagManager
|
2022-01-13 16:24:37 +01:00
|
|
|
private let dnsChecker: DNSCheckerType.Type
|
2023-07-05 16:39:04 -07:00
|
|
|
private let signing: SigningType
|
2024-05-16 15:13:40 +02:00
|
|
|
private let diagnosticsTracker: DiagnosticsTrackerType?
|
|
|
|
|
private let dateProvider: DateProvider
|
2024-07-23 12:41:16 -05:00
|
|
|
private let retriableStatusCodes: Set<HTTPStatusCode>
|
|
|
|
|
private let operationDispatcher: OperationDispatcher
|
|
|
|
|
|
|
|
|
|
private let retryBackoffIntervals: [TimeInterval] = [
|
|
|
|
|
TimeInterval(0),
|
|
|
|
|
TimeInterval(0.75),
|
|
|
|
|
TimeInterval(3)
|
|
|
|
|
]
|
2021-07-30 23:03:25 +02:00
|
|
|
|
2022-06-14 15:12:46 -07:00
|
|
|
init(apiKey: String,
|
|
|
|
|
systemInfo: SystemInfo,
|
|
|
|
|
eTagManager: ETagManager,
|
2023-07-05 16:39:04 -07:00
|
|
|
signing: SigningType,
|
2024-05-16 15:13:40 +02:00
|
|
|
diagnosticsTracker: DiagnosticsTrackerType?,
|
2022-06-14 15:12:46 -07:00
|
|
|
dnsChecker: DNSCheckerType.Type = DNSChecker.self,
|
2024-07-23 12:41:16 -05:00
|
|
|
retriableStatusCodes: Set<HTTPStatusCode> = Set([.tooManyRequests]),
|
2024-05-16 15:13:40 +02:00
|
|
|
requestTimeout: TimeInterval = Configuration.networkTimeoutDefault,
|
2024-07-23 12:41:16 -05:00
|
|
|
dateProvider: DateProvider = DateProvider(),
|
|
|
|
|
operationDispatcher: OperationDispatcher) {
|
2021-07-30 23:03:25 +02:00
|
|
|
let config = URLSessionConfiguration.ephemeral
|
|
|
|
|
config.httpMaximumConnectionsPerHost = 1
|
2022-05-12 15:43:10 -07:00
|
|
|
config.timeoutIntervalForRequest = requestTimeout
|
|
|
|
|
config.timeoutIntervalForResource = requestTimeout
|
2023-06-16 10:26:39 -07:00
|
|
|
config.urlCache = nil // We implement our own caching with `ETagManager`.
|
2023-03-23 14:34:14 -07:00
|
|
|
self.session = URLSession(configuration: config,
|
|
|
|
|
delegate: RedirectLoggerSessionDelegate(),
|
|
|
|
|
delegateQueue: nil)
|
2021-07-30 23:03:25 +02:00
|
|
|
self.systemInfo = systemInfo
|
|
|
|
|
self.eTagManager = eTagManager
|
2023-02-16 03:06:59 -08:00
|
|
|
self.signing = signing
|
2024-05-16 15:13:40 +02:00
|
|
|
self.diagnosticsTracker = diagnosticsTracker
|
2023-07-05 16:39:04 -07:00
|
|
|
self.dnsChecker = dnsChecker
|
2024-07-23 12:41:16 -05:00
|
|
|
self.retriableStatusCodes = retriableStatusCodes
|
2022-05-12 15:43:10 -07:00
|
|
|
self.timeout = requestTimeout
|
2022-06-14 15:12:46 -07:00
|
|
|
self.apiKey = apiKey
|
2022-10-20 13:03:15 -05:00
|
|
|
self.authHeaders = HTTPClient.authorizationHeader(withAPIKey: apiKey)
|
2024-05-16 15:13:40 +02:00
|
|
|
self.dateProvider = dateProvider
|
2024-07-23 12:41:16 -05:00
|
|
|
self.operationDispatcher = operationDispatcher
|
2021-07-30 23:03:25 +02:00
|
|
|
}
|
|
|
|
|
|
2023-02-22 09:20:25 -08:00
|
|
|
/// - Parameter verificationMode: if `nil`, this will default to `SystemInfo.responseVerificationMode`
|
|
|
|
|
func perform<Value: HTTPResponseBody>(
|
|
|
|
|
_ request: HTTPRequest,
|
|
|
|
|
with verificationMode: Signing.ResponseVerificationMode? = nil,
|
|
|
|
|
completionHandler: Completion<Value>?
|
|
|
|
|
) {
|
2023-03-23 14:21:47 -07:00
|
|
|
#if DEBUG
|
2023-05-19 10:54:42 -07:00
|
|
|
guard !self.systemInfo.dangerousSettings.internalSettings.forceServerErrors else {
|
|
|
|
|
Logger.warn(Strings.network.api_request_forcing_server_error(request))
|
2023-09-07 12:25:19 -07:00
|
|
|
|
|
|
|
|
// `FB13133387`: when computing offline CustomerInfo, `StoreKit.Transaction.unfinished`
|
|
|
|
|
// might be empty if called immediately after `Product.purchase()`.
|
|
|
|
|
// This introduces a delay to simulate a real API request, and avoid that race condition.
|
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) {
|
|
|
|
|
completionHandler?(
|
|
|
|
|
.failure(.errorResponse(Self.serverErrorResponse, .internalServerError))
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 14:21:47 -07:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
2022-10-20 13:03:15 -05:00
|
|
|
self.perform(request: .init(httpRequest: request,
|
2023-02-08 09:05:20 -08:00
|
|
|
authHeaders: self.authHeaders,
|
2023-11-17 12:19:49 -08:00
|
|
|
defaultHeaders: self.defaultHeaders,
|
2023-02-22 09:20:25 -08:00
|
|
|
verificationMode: verificationMode ?? self.systemInfo.responseVerificationMode,
|
2023-12-21 12:47:34 -08:00
|
|
|
internalSettings: self.systemInfo.dangerousSettings.internalSettings,
|
2022-10-20 13:03:15 -05:00
|
|
|
completionHandler: completionHandler))
|
2021-07-30 23:03:25 +02:00
|
|
|
}
|
|
|
|
|
|
2021-08-23 11:20:55 -07:00
|
|
|
func clearCaches() {
|
2022-10-20 13:03:15 -05:00
|
|
|
self.eTagManager.clearCaches()
|
2021-07-30 23:03:25 +02:00
|
|
|
}
|
|
|
|
|
|
2023-03-13 11:57:18 -07:00
|
|
|
var signatureVerificationEnabled: Bool {
|
|
|
|
|
return self.systemInfo.responseVerificationMode.isEnabled
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-17 12:19:49 -08:00
|
|
|
// Visible for tests
|
|
|
|
|
var defaultHeaders: RequestHeaders {
|
2024-07-02 17:32:50 +02:00
|
|
|
let preferredLanguages = self.systemInfo.preferredLanguages.prefix(3).map {
|
|
|
|
|
$0.replacingOccurrences(of: "-", with: "_")
|
|
|
|
|
}.joined(separator: ",")
|
2023-11-17 12:19:49 -08:00
|
|
|
var headers: RequestHeaders = [
|
|
|
|
|
"content-type": "application/json",
|
|
|
|
|
"X-Version": SystemInfo.frameworkVersion,
|
|
|
|
|
"X-Platform": SystemInfo.platformHeader,
|
|
|
|
|
"X-Platform-Version": SystemInfo.systemVersion,
|
|
|
|
|
"X-Platform-Flavor": self.systemInfo.platformFlavor,
|
|
|
|
|
"X-Client-Version": SystemInfo.appVersion,
|
|
|
|
|
"X-Client-Build-Version": SystemInfo.buildVersion,
|
|
|
|
|
"X-Client-Bundle-ID": SystemInfo.bundleIdentifier,
|
2024-07-02 17:32:50 +02:00
|
|
|
"X-Preferred-Locales": preferredLanguages,
|
2024-07-01 12:37:21 -07:00
|
|
|
"X-StoreKit2-Enabled": "\(self.systemInfo.storeKitVersion.isStoreKit2EnabledAndAvailable)",
|
|
|
|
|
"X-StoreKit-Version": "\(self.systemInfo.storeKitVersion.effectiveVersion)",
|
2023-11-17 12:19:49 -08:00
|
|
|
"X-Observer-Mode-Enabled": "\(self.systemInfo.observerMode)",
|
2024-07-23 12:41:16 -05:00
|
|
|
RequestHeader.retryCount.rawValue: "0",
|
2023-11-17 12:19:49 -08:00
|
|
|
RequestHeader.sandbox.rawValue: "\(self.systemInfo.isSandbox)"
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
if let storefront = self.systemInfo.storefront {
|
|
|
|
|
headers["X-Storefront"] = storefront.countryCode
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let platformFlavorVersion = self.systemInfo.platformFlavorVersion {
|
|
|
|
|
headers["X-Platform-Flavor-Version"] = platformFlavorVersion
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let idfv = self.systemInfo.identifierForVendor {
|
|
|
|
|
headers["X-Apple-Device-Identifier"] = idfv
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.systemInfo.dangerousSettings.customEntitlementComputation {
|
|
|
|
|
headers["X-Custom-Entitlements-Computation"] = "\(true)"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return headers
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-30 23:03:25 +02:00
|
|
|
}
|
|
|
|
|
|
2022-03-04 13:46:24 -08:00
|
|
|
extension HTTPClient {
|
|
|
|
|
|
|
|
|
|
static func authorizationHeader(withAPIKey apiKey: String) -> RequestHeaders {
|
2023-02-27 09:07:23 -08:00
|
|
|
return [RequestHeader.authorization.rawValue: "Bearer \(apiKey)"]
|
2022-03-04 13:46:24 -08:00
|
|
|
}
|
|
|
|
|
|
2023-02-08 09:05:20 -08:00
|
|
|
static func nonceHeader(with data: Data) -> RequestHeaders {
|
2023-02-27 09:07:23 -08:00
|
|
|
return [RequestHeader.nonce.rawValue: data.base64EncodedString()]
|
2023-02-08 09:05:20 -08:00
|
|
|
}
|
|
|
|
|
|
2023-07-13 07:45:16 -07:00
|
|
|
static func postParametersHeaderForSigning(with body: HTTPRequestBody) -> RequestHeaders {
|
|
|
|
|
if let header = body.postParameterHeader {
|
|
|
|
|
return [RequestHeader.postParameters.rawValue: header]
|
|
|
|
|
} else {
|
|
|
|
|
return [:]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-21 11:53:10 -08:00
|
|
|
static func headerParametersForSignatureHeader(
|
|
|
|
|
with headers: RequestHeaders,
|
|
|
|
|
path: HTTPRequestPath
|
|
|
|
|
) -> RequestHeaders {
|
|
|
|
|
if let header = HTTPRequest.headerParametersForSignatureHeader(
|
|
|
|
|
headers: headers,
|
|
|
|
|
path: path
|
|
|
|
|
) {
|
|
|
|
|
return [RequestHeader.headerParametersForSignature.rawValue: header]
|
|
|
|
|
} else {
|
|
|
|
|
return [:]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-27 09:07:23 -08:00
|
|
|
enum RequestHeader: String {
|
|
|
|
|
|
|
|
|
|
case authorization = "Authorization"
|
|
|
|
|
case nonce = "X-Nonce"
|
|
|
|
|
case eTag = "X-RevenueCat-ETag"
|
2023-04-14 14:32:24 -07:00
|
|
|
case eTagValidationTime = "X-RC-Last-Refresh-Time"
|
2023-07-13 07:45:16 -07:00
|
|
|
case postParameters = "X-Post-Params-Hash"
|
2023-12-21 11:53:10 -08:00
|
|
|
case headerParametersForSignature = "X-Headers-Hash"
|
2023-11-17 12:19:49 -08:00
|
|
|
case sandbox = "X-Is-Sandbox"
|
2024-07-23 12:41:16 -05:00
|
|
|
case retryCount = "X-Retry-Count"
|
2023-02-27 09:07:23 -08:00
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enum ResponseHeader: String {
|
|
|
|
|
|
|
|
|
|
case eTag = "X-RevenueCat-ETag"
|
2023-06-21 11:02:45 -07:00
|
|
|
case location = "Location"
|
2023-02-27 09:07:23 -08:00
|
|
|
case signature = "X-Signature"
|
2023-03-01 09:24:32 -08:00
|
|
|
case requestDate = "X-RevenueCat-Request-Time"
|
2023-05-24 09:51:50 -07:00
|
|
|
case contentType = "Content-Type"
|
2023-06-19 08:16:57 -07:00
|
|
|
case isLoadShedder = "X-RevenueCat-Fortress"
|
2023-10-25 20:24:23 -07:00
|
|
|
case requestID = "X-Request-ID"
|
|
|
|
|
case amazonTraceID = "X-Amzn-Trace-ID"
|
2024-07-23 12:41:16 -05:00
|
|
|
case retryAfter = "Retry-After"
|
|
|
|
|
case isRetryable = "Is-Retryable"
|
2023-02-27 09:07:23 -08:00
|
|
|
|
|
|
|
|
}
|
2023-02-08 09:05:20 -08:00
|
|
|
|
2022-03-04 13:46:24 -08:00
|
|
|
}
|
|
|
|
|
|
2022-08-31 13:30:38 -07:00
|
|
|
// @unchecked because:
|
|
|
|
|
// - Class is not `final` (it's mocked). This implicitly makes subclasses `Sendable` even if they're not thread-safe.
|
|
|
|
|
extension HTTPClient: @unchecked Sendable {}
|
|
|
|
|
|
|
|
|
|
// MARK: - Private
|
|
|
|
|
|
2024-07-23 12:41:16 -05:00
|
|
|
internal extension HTTPClient {
|
2022-04-01 14:22:29 -07:00
|
|
|
|
2022-02-23 18:05:59 -05:00
|
|
|
struct State {
|
|
|
|
|
var queuedRequests: [Request]
|
|
|
|
|
var currentSerialRequest: Request?
|
|
|
|
|
|
2022-06-14 15:12:46 -07:00
|
|
|
static let initial: Self = .init(queuedRequests: [], currentSerialRequest: nil)
|
2022-02-23 18:05:59 -05:00
|
|
|
}
|
2022-04-01 14:22:29 -07:00
|
|
|
|
2022-02-16 13:41:28 -08:00
|
|
|
struct Request: CustomStringConvertible {
|
|
|
|
|
|
2022-03-04 13:42:23 -08:00
|
|
|
var httpRequest: HTTPRequest
|
2022-03-04 13:46:24 -08:00
|
|
|
var headers: HTTPClient.RequestHeaders
|
2023-02-22 09:20:25 -08:00
|
|
|
var verificationMode: Signing.ResponseVerificationMode
|
2022-04-12 15:11:22 -07:00
|
|
|
var completionHandler: HTTPClient.Completion<Data>?
|
2024-07-23 12:41:16 -05:00
|
|
|
|
|
|
|
|
/// Whether the request has been retried.
|
|
|
|
|
var retried: Bool {
|
|
|
|
|
return self.retryCount > 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The number of times that we have retried the request
|
|
|
|
|
var retryCount: UInt = 0
|
2022-02-16 13:41:28 -08:00
|
|
|
|
2022-06-14 15:12:46 -07:00
|
|
|
init<Value: HTTPResponseBody>(httpRequest: HTTPRequest,
|
2023-02-08 09:05:20 -08:00
|
|
|
authHeaders: HTTPClient.RequestHeaders,
|
2023-11-17 12:19:49 -08:00
|
|
|
defaultHeaders: HTTPClient.RequestHeaders,
|
2023-02-22 09:20:25 -08:00
|
|
|
verificationMode: Signing.ResponseVerificationMode,
|
2023-12-21 12:47:34 -08:00
|
|
|
internalSettings: InternalDangerousSettingsType,
|
2022-06-14 15:12:46 -07:00
|
|
|
completionHandler: HTTPClient.Completion<Value>?) {
|
2023-02-22 09:20:25 -08:00
|
|
|
self.httpRequest = httpRequest.requestAddingNonceIfRequired(with: verificationMode)
|
2023-11-17 12:19:49 -08:00
|
|
|
self.headers = self.httpRequest.headers(
|
|
|
|
|
with: authHeaders,
|
|
|
|
|
defaultHeaders: defaultHeaders,
|
2023-12-21 12:47:34 -08:00
|
|
|
verificationMode: verificationMode,
|
|
|
|
|
internalSettings: internalSettings
|
2023-11-17 12:19:49 -08:00
|
|
|
)
|
2023-02-22 09:20:25 -08:00
|
|
|
self.verificationMode = verificationMode
|
2022-04-12 15:11:22 -07:00
|
|
|
|
|
|
|
|
if let completionHandler = completionHandler {
|
|
|
|
|
self.completionHandler = { result in
|
|
|
|
|
completionHandler(result.parseResponse())
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
self.completionHandler = nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-04 13:42:23 -08:00
|
|
|
var method: HTTPRequest.Method { self.httpRequest.method }
|
2023-08-10 15:23:13 -07:00
|
|
|
var path: String { self.httpRequest.path.relativePath }
|
2022-02-16 13:41:28 -08:00
|
|
|
|
|
|
|
|
func retriedRequest() -> Self {
|
|
|
|
|
var copy = self
|
2024-07-23 12:41:16 -05:00
|
|
|
copy.retryCount += 1
|
|
|
|
|
copy.headers[RequestHeader.retryCount.rawValue] = "\(copy.retryCount)"
|
2022-02-16 13:41:28 -08:00
|
|
|
|
|
|
|
|
return copy
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var description: String {
|
|
|
|
|
"""
|
|
|
|
|
<\(type(of: self)): httpMethod=\(self.method.httpMethod)
|
|
|
|
|
path=\(self.path)
|
|
|
|
|
headers=\(self.headers.description )
|
|
|
|
|
retried=\(self.retried)
|
|
|
|
|
>
|
|
|
|
|
"""
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-30 23:03:25 +02:00
|
|
|
private extension HTTPClient {
|
|
|
|
|
|
2023-03-23 14:21:47 -07:00
|
|
|
static let serverErrorResponse: ErrorResponse = .init(code: .internalServerError,
|
|
|
|
|
originalCode: BackendErrorCode.unknownBackendError.rawValue)
|
|
|
|
|
|
2022-02-23 18:09:50 -05:00
|
|
|
func perform(request: Request) {
|
|
|
|
|
if !request.retried {
|
2022-02-23 18:05:59 -05:00
|
|
|
let requestEnqueued: Bool = self.state.modify {
|
|
|
|
|
if $0.currentSerialRequest != nil {
|
|
|
|
|
Logger.debug(Strings.network.serial_request_queued(httpMethod: request.method.httpMethod,
|
|
|
|
|
path: request.path,
|
|
|
|
|
queuedRequestsCount: $0.queuedRequests.count))
|
|
|
|
|
|
|
|
|
|
$0.queuedRequests.append(request)
|
|
|
|
|
return true
|
|
|
|
|
} else {
|
|
|
|
|
Logger.debug(Strings.network.starting_request(httpMethod: request.method.httpMethod,
|
|
|
|
|
path: request.path))
|
|
|
|
|
$0.currentSerialRequest = request
|
|
|
|
|
return false
|
|
|
|
|
}
|
2021-07-30 23:03:25 +02:00
|
|
|
}
|
2022-02-23 18:05:59 -05:00
|
|
|
|
|
|
|
|
guard !requestEnqueued else { return }
|
2021-07-30 23:03:25 +02:00
|
|
|
}
|
|
|
|
|
|
2022-02-23 18:09:50 -05:00
|
|
|
self.start(request: request)
|
2021-07-30 23:03:25 +02:00
|
|
|
}
|
|
|
|
|
|
2022-04-12 15:11:22 -07:00
|
|
|
/// - Returns: `nil` if the request must be retried
|
2024-05-16 15:13:40 +02:00
|
|
|
// swiftlint:disable:next function_parameter_count
|
2022-04-01 14:22:29 -07:00
|
|
|
func parse(urlResponse: URLResponse?,
|
|
|
|
|
request: Request,
|
|
|
|
|
urlRequest: URLRequest,
|
|
|
|
|
data: Data?,
|
2024-05-16 15:13:40 +02:00
|
|
|
error networkError: Error?,
|
|
|
|
|
requestStartTime: Date) -> VerifiedHTTPResponse<Data>.Result? {
|
2022-04-01 14:22:29 -07:00
|
|
|
if let networkError = networkError {
|
2022-04-21 09:31:20 -07:00
|
|
|
return .failure(NetworkError(networkError, dnsChecker: self.dnsChecker))
|
2022-04-01 14:22:29 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard let httpURLResponse = urlResponse as? HTTPURLResponse else {
|
2022-04-12 15:11:22 -07:00
|
|
|
return .failure(.unexpectedResponse(urlResponse))
|
2022-04-01 14:22:29 -07:00
|
|
|
}
|
|
|
|
|
|
2023-06-30 12:24:53 -07:00
|
|
|
let statusCode: HTTPStatusCode = .init(rawValue: httpURLResponse.statusCode)
|
2022-04-01 14:22:29 -07:00
|
|
|
|
2023-06-30 12:24:53 -07:00
|
|
|
// `nil` if status code is 304, since the response will be empty and fetched from the eTag.
|
|
|
|
|
let dataIfAvailable = statusCode == .notModified
|
|
|
|
|
? nil
|
|
|
|
|
: data
|
2022-04-01 14:22:29 -07:00
|
|
|
|
2023-06-30 12:24:53 -07:00
|
|
|
return self.createVerifiedResponse(request: request,
|
|
|
|
|
urlRequest: urlRequest,
|
|
|
|
|
data: dataIfAvailable,
|
2024-05-16 15:13:40 +02:00
|
|
|
response: httpURLResponse,
|
|
|
|
|
requestStartTime: requestStartTime)
|
2023-06-30 12:24:53 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// - Returns `Result<VerifiedHTTPResponse<Data>, NetworkError>?`
|
|
|
|
|
private func createVerifiedResponse(
|
|
|
|
|
request: Request,
|
|
|
|
|
urlRequest: URLRequest,
|
|
|
|
|
data: Data?,
|
2024-05-16 15:13:40 +02:00
|
|
|
response httpURLResponse: HTTPURLResponse,
|
|
|
|
|
requestStartTime: Date
|
2023-06-30 12:24:53 -07:00
|
|
|
) -> VerifiedHTTPResponse<Data>.Result? {
|
2023-12-21 12:47:34 -08:00
|
|
|
#if DEBUG
|
|
|
|
|
let requestHeaders: HTTPClient.RequestHeaders
|
|
|
|
|
|
|
|
|
|
if self.systemInfo.dangerousSettings.internalSettings.disableHeaderSignatureVerification {
|
|
|
|
|
Logger.warn(Strings.network.api_request_disabling_header_parameter_signature_verification(
|
|
|
|
|
request.httpRequest
|
|
|
|
|
))
|
|
|
|
|
requestHeaders = [:]
|
|
|
|
|
} else {
|
|
|
|
|
requestHeaders = request.headers
|
|
|
|
|
}
|
|
|
|
|
#else
|
|
|
|
|
let requestHeaders = request.headers
|
|
|
|
|
#endif
|
|
|
|
|
|
2024-05-16 15:13:40 +02:00
|
|
|
let result = Result
|
2023-06-30 12:24:53 -07:00
|
|
|
.success(data)
|
|
|
|
|
.mapToResponse(response: httpURLResponse, request: request.httpRequest)
|
|
|
|
|
// Verify response
|
|
|
|
|
.map { cachedResponse -> VerifiedHTTPResponse<Data?> in
|
|
|
|
|
return cachedResponse.verify(
|
2023-07-05 16:39:04 -07:00
|
|
|
signing: self.signing(for: request.httpRequest),
|
2023-06-30 12:24:53 -07:00
|
|
|
request: request.httpRequest,
|
2023-12-21 12:47:34 -08:00
|
|
|
requestHeaders: requestHeaders,
|
2023-07-05 16:39:04 -07:00
|
|
|
publicKey: request.verificationMode.publicKey
|
2023-06-30 12:24:53 -07:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
// Fetch from ETagManager if available
|
|
|
|
|
.map { (response) -> VerifiedHTTPResponse<Data>? in
|
|
|
|
|
return self.eTagManager.httpResultFromCacheOrBackend(
|
2023-03-01 09:24:32 -08:00
|
|
|
with: response,
|
|
|
|
|
request: urlRequest,
|
|
|
|
|
retried: request.retried
|
2023-06-30 12:24:53 -07:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
// Upgrade to error in enforced mode
|
|
|
|
|
.flatMap { response -> Result<VerifiedHTTPResponse<Data>?, NetworkError> in
|
2023-07-07 14:05:55 -07:00
|
|
|
if let response = response, response.verificationResult == .failed {
|
|
|
|
|
if case .enforced = request.verificationMode {
|
|
|
|
|
return .failure(.signatureVerificationFailed(path: request.httpRequest.path,
|
2023-10-25 20:24:23 -07:00
|
|
|
code: response.httpStatusCode))
|
2023-07-07 14:05:55 -07:00
|
|
|
} else {
|
|
|
|
|
// Any other mode gets forwarded as a success, but we log the error
|
|
|
|
|
Logger.error(Strings.signing.request_failed_verification(request.httpRequest))
|
|
|
|
|
return .success(response)
|
|
|
|
|
}
|
2023-06-30 12:24:53 -07:00
|
|
|
} else {
|
|
|
|
|
return .success(response)
|
2023-03-01 09:24:32 -08:00
|
|
|
}
|
2022-04-01 14:22:29 -07:00
|
|
|
}
|
2022-04-05 11:39:49 -07:00
|
|
|
.asOptionalResult?
|
2022-04-12 15:11:22 -07:00
|
|
|
.convertUnsuccessfulResponseToError()
|
2024-05-16 15:13:40 +02:00
|
|
|
|
|
|
|
|
self.trackHttpRequestPerformedIfNeeded(request: request,
|
|
|
|
|
requestStartTime: requestStartTime,
|
|
|
|
|
result: result)
|
|
|
|
|
|
|
|
|
|
return result
|
2022-04-01 14:22:29 -07:00
|
|
|
}
|
|
|
|
|
|
2024-05-16 15:13:40 +02:00
|
|
|
// swiftlint:disable:next function_parameter_count
|
2022-02-16 13:41:28 -08:00
|
|
|
func handle(urlResponse: URLResponse?,
|
|
|
|
|
request: Request,
|
|
|
|
|
urlRequest: URLRequest,
|
|
|
|
|
data: Data?,
|
2024-05-16 15:13:40 +02:00
|
|
|
error networkError: Error?,
|
|
|
|
|
requestStartTime: Date) {
|
2023-05-19 11:58:12 -07:00
|
|
|
RCTestAssertNotMainThread()
|
|
|
|
|
|
2022-04-05 11:39:49 -07:00
|
|
|
let response = self.parse(
|
2022-04-01 14:22:29 -07:00
|
|
|
urlResponse: urlResponse,
|
|
|
|
|
request: request,
|
|
|
|
|
urlRequest: urlRequest,
|
|
|
|
|
data: data,
|
2024-05-16 15:13:40 +02:00
|
|
|
error: networkError,
|
|
|
|
|
requestStartTime: requestStartTime
|
2022-04-01 14:22:29 -07:00
|
|
|
)
|
2021-07-30 23:03:25 +02:00
|
|
|
|
2022-04-05 11:39:49 -07:00
|
|
|
if let response = response {
|
2023-10-25 20:24:23 -07:00
|
|
|
let httpURLResponse = urlResponse as? HTTPURLResponse
|
2024-07-23 12:41:16 -05:00
|
|
|
var requestRetryScheduled = false
|
2023-10-25 20:24:23 -07:00
|
|
|
|
2022-11-14 09:15:25 -08:00
|
|
|
switch response {
|
|
|
|
|
case let .success(response):
|
2023-05-15 19:28:25 -04:00
|
|
|
Logger.debug(Strings.network.api_request_completed(
|
|
|
|
|
request.httpRequest,
|
|
|
|
|
// Getting status code from the original response to detect 304s
|
|
|
|
|
// If that can't be extracted, get status code from the parsed response.
|
2023-10-25 20:24:23 -07:00
|
|
|
httpCode: httpURLResponse?.httpStatusCode ?? response.httpStatusCode,
|
|
|
|
|
metadata: Logger.verboseLogsEnabled ? response.metadata : nil
|
2023-05-15 19:28:25 -04:00
|
|
|
))
|
2023-06-19 08:16:57 -07:00
|
|
|
|
|
|
|
|
if response.isLoadShedder {
|
|
|
|
|
Logger.debug(Strings.network.request_handled_by_load_shedder(request.httpRequest.path))
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-14 09:15:25 -08:00
|
|
|
case let .failure(error):
|
2023-10-25 20:24:23 -07:00
|
|
|
let httpURLResponse = urlResponse as? HTTPURLResponse
|
|
|
|
|
|
|
|
|
|
Logger.debug(Strings.network.api_request_failed(
|
|
|
|
|
request.httpRequest,
|
|
|
|
|
httpCode: httpURLResponse?.httpStatusCode,
|
|
|
|
|
error: error,
|
|
|
|
|
metadata: httpURLResponse?.metadata)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if httpURLResponse?.isLoadShedder == true {
|
|
|
|
|
Logger.debug(Strings.network.request_handled_by_load_shedder(request.httpRequest.path))
|
|
|
|
|
}
|
2024-07-23 12:41:16 -05:00
|
|
|
|
|
|
|
|
requestRetryScheduled = self.retryRequestIfNeeded(request: request, httpURLResponse: httpURLResponse)
|
2022-11-14 09:15:25 -08:00
|
|
|
}
|
|
|
|
|
|
2024-07-23 12:41:16 -05:00
|
|
|
if !requestRetryScheduled {
|
|
|
|
|
request.completionHandler?(response)
|
|
|
|
|
}
|
2022-04-05 11:39:49 -07:00
|
|
|
} else {
|
2022-04-01 14:22:29 -07:00
|
|
|
Logger.debug(Strings.network.retrying_request(httpMethod: request.method.httpMethod,
|
|
|
|
|
path: request.path))
|
2021-07-30 23:03:25 +02:00
|
|
|
|
2022-04-01 14:22:29 -07:00
|
|
|
self.state.modify {
|
|
|
|
|
$0.queuedRequests.insert(request.retriedRequest(), at: 0)
|
2021-07-30 23:03:25 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-23 18:09:50 -05:00
|
|
|
self.beginNextRequest()
|
2021-07-30 23:03:25 +02:00
|
|
|
}
|
|
|
|
|
|
2022-02-23 18:05:59 -05:00
|
|
|
func beginNextRequest() {
|
|
|
|
|
let nextRequest: Request? = self.state.modify {
|
|
|
|
|
Logger.debug(Strings.network.serial_request_done(httpMethod: $0.currentSerialRequest?.method.httpMethod,
|
|
|
|
|
path: $0.currentSerialRequest?.path,
|
|
|
|
|
queuedRequestsCount: $0.queuedRequests.count))
|
|
|
|
|
$0.currentSerialRequest = $0.queuedRequests.popFirst()
|
|
|
|
|
|
|
|
|
|
return $0.currentSerialRequest
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let nextRequest = nextRequest {
|
|
|
|
|
Logger.debug(Strings.network.starting_next_request(request: nextRequest.description))
|
2022-02-23 18:09:50 -05:00
|
|
|
self.start(request: nextRequest)
|
2022-02-23 18:05:59 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-23 18:09:50 -05:00
|
|
|
func start(request: Request) {
|
2023-11-17 12:19:49 -08:00
|
|
|
let urlRequest = self.convert(request: request)
|
2022-02-23 18:05:59 -05:00
|
|
|
|
|
|
|
|
guard let urlRequest = urlRequest else {
|
2023-06-16 09:19:16 -07:00
|
|
|
let error: NetworkError = .unableToCreateRequest(request.httpRequest.path)
|
2022-02-23 18:05:59 -05:00
|
|
|
|
2023-06-16 09:19:16 -07:00
|
|
|
Logger.error(error.description)
|
|
|
|
|
request.completionHandler?(.failure(error))
|
2022-02-23 18:05:59 -05:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-15 14:32:54 -05:00
|
|
|
Logger.debug(Strings.network.api_request_started(request.httpRequest))
|
2022-02-23 18:05:59 -05:00
|
|
|
|
2024-05-16 15:13:40 +02:00
|
|
|
let requestStartTime = self.dateProvider.now()
|
|
|
|
|
|
2024-05-18 08:21:02 -05:00
|
|
|
// swiftlint:disable:next redundant_void_return
|
2023-03-23 14:34:14 -07:00
|
|
|
let task = self.session.dataTask(with: urlRequest) { (data, urlResponse, error) -> Void in
|
2022-02-23 18:05:59 -05:00
|
|
|
self.handle(urlResponse: urlResponse,
|
|
|
|
|
request: request,
|
|
|
|
|
urlRequest: urlRequest,
|
|
|
|
|
data: data,
|
2024-05-16 15:13:40 +02:00
|
|
|
error: error,
|
|
|
|
|
requestStartTime: requestStartTime)
|
2022-02-23 18:05:59 -05:00
|
|
|
}
|
|
|
|
|
task.resume()
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-16 13:41:28 -08:00
|
|
|
func convert(request: Request) -> URLRequest? {
|
2023-08-10 15:23:13 -07:00
|
|
|
guard let requestURL = request.httpRequest.path.url(proxyURL: SystemInfo.proxyURL) else {
|
2021-07-30 23:03:25 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
var urlRequest = URLRequest(url: requestURL)
|
2022-02-16 13:41:28 -08:00
|
|
|
urlRequest.httpMethod = request.method.httpMethod
|
2023-02-13 07:02:18 -08:00
|
|
|
urlRequest.allHTTPHeaderFields = self.headers(for: request, urlRequest: urlRequest)
|
2021-07-30 23:03:25 +02:00
|
|
|
|
2022-03-08 12:56:13 -08:00
|
|
|
do {
|
2023-05-31 07:11:23 -07:00
|
|
|
urlRequest.httpBody = try request.httpRequest.requestBody?.jsonEncodedData
|
2022-03-08 12:56:13 -08:00
|
|
|
} catch {
|
|
|
|
|
Logger.error(Strings.network.creating_json_error(error: error.localizedDescription))
|
|
|
|
|
return nil
|
2021-07-30 23:03:25 +02:00
|
|
|
}
|
2022-02-16 13:41:28 -08:00
|
|
|
|
2021-07-30 23:03:25 +02:00
|
|
|
return urlRequest
|
|
|
|
|
}
|
2023-02-13 07:02:18 -08:00
|
|
|
|
|
|
|
|
private func headers(for request: Request, urlRequest: URLRequest) -> HTTPClient.RequestHeaders {
|
|
|
|
|
if request.httpRequest.path.shouldSendEtag {
|
2023-02-16 03:06:59 -08:00
|
|
|
let eTagHeader = self.eTagManager.eTagHeader(
|
|
|
|
|
for: urlRequest,
|
2023-06-30 12:24:53 -07:00
|
|
|
withSignatureVerification: request.verificationMode.isEnabled,
|
2023-03-13 12:24:02 -07:00
|
|
|
refreshETag: request.retried
|
2023-02-16 03:06:59 -08:00
|
|
|
)
|
2023-02-13 07:02:18 -08:00
|
|
|
return request.headers.merging(eTagHeader)
|
|
|
|
|
} else {
|
|
|
|
|
return request.headers
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-08-24 12:56:37 -07:00
|
|
|
|
2023-07-05 16:39:04 -07:00
|
|
|
private func signing(for request: HTTPRequest) -> SigningType {
|
2023-06-13 13:43:16 -07:00
|
|
|
#if DEBUG
|
|
|
|
|
if self.systemInfo.dangerousSettings.internalSettings.forceSignatureFailures {
|
|
|
|
|
Logger.warn(Strings.network.api_request_forcing_signature_failure(request))
|
2023-07-05 16:39:04 -07:00
|
|
|
return FakeSigning.default
|
2023-06-13 13:43:16 -07:00
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
return self.signing
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-16 15:13:40 +02:00
|
|
|
private func trackHttpRequestPerformedIfNeeded(request: Request,
|
|
|
|
|
requestStartTime: Date,
|
|
|
|
|
result: Result<VerifiedHTTPResponse<Data>, NetworkError>?) {
|
|
|
|
|
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) {
|
|
|
|
|
guard let diagnosticsTracker = self.diagnosticsTracker, let result else { return }
|
|
|
|
|
let responseTime = self.dateProvider.now().timeIntervalSince(requestStartTime)
|
|
|
|
|
let requestPathName = request.httpRequest.path.name
|
|
|
|
|
Task(priority: .background) {
|
|
|
|
|
switch result {
|
|
|
|
|
case let .success(response):
|
|
|
|
|
let httpStatusCode = response.httpStatusCode.rawValue
|
|
|
|
|
let verificationResult = response.verificationResult
|
|
|
|
|
await diagnosticsTracker.trackHttpRequestPerformed(endpointName: requestPathName,
|
|
|
|
|
responseTime: responseTime,
|
|
|
|
|
wasSuccessful: true,
|
|
|
|
|
responseCode: httpStatusCode,
|
2024-09-04 09:03:34 +02:00
|
|
|
backendErrorCode: nil,
|
2024-05-16 15:13:40 +02:00
|
|
|
resultOrigin: response.origin,
|
|
|
|
|
verificationResult: verificationResult)
|
|
|
|
|
case let .failure(error):
|
|
|
|
|
var responseCode = -1
|
2024-09-04 09:03:34 +02:00
|
|
|
var backendErrorCode: Int?
|
|
|
|
|
if case let .errorResponse(errorResponse, code, _) = error {
|
2024-05-16 15:13:40 +02:00
|
|
|
responseCode = code.rawValue
|
2024-09-04 09:03:34 +02:00
|
|
|
backendErrorCode = errorResponse.code.rawValue
|
2024-05-16 15:13:40 +02:00
|
|
|
}
|
|
|
|
|
await diagnosticsTracker.trackHttpRequestPerformed(endpointName: requestPathName,
|
|
|
|
|
responseTime: responseTime,
|
|
|
|
|
wasSuccessful: false,
|
|
|
|
|
responseCode: responseCode,
|
2024-09-04 09:03:34 +02:00
|
|
|
backendErrorCode: backendErrorCode,
|
2024-05-16 15:13:40 +02:00
|
|
|
resultOrigin: nil,
|
|
|
|
|
verificationResult: .notRequested)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-07-23 12:41:16 -05:00
|
|
|
}
|
2024-05-16 15:13:40 +02:00
|
|
|
|
2024-07-23 12:41:16 -05:00
|
|
|
// MARK: - Request Retry Logic
|
|
|
|
|
extension HTTPClient {
|
|
|
|
|
|
|
|
|
|
/// Evaluates whether a request should be retried and schedules a retry if necessary.
|
|
|
|
|
///
|
|
|
|
|
/// This function checks the HTTP response status code to determine if the request should be retried.
|
|
|
|
|
/// If the retry conditions are met, it schedules the request to be retried after a backoff interval.
|
|
|
|
|
///
|
|
|
|
|
/// - Parameters:
|
|
|
|
|
/// - request: The original `HTTPClient.Request` that may need to be retried.
|
|
|
|
|
/// - httpURLResponse: An optional `HTTPURLResponse` that contains the status code of the response.
|
|
|
|
|
/// - Returns: A Boolean value indicating whether the request was scheduled for a retry.
|
|
|
|
|
internal func retryRequestIfNeeded(
|
|
|
|
|
request: HTTPClient.Request,
|
|
|
|
|
httpURLResponse: HTTPURLResponse?
|
|
|
|
|
) -> Bool {
|
2024-07-25 16:46:31 -05:00
|
|
|
|
|
|
|
|
guard request.httpRequest.isRetryable,
|
|
|
|
|
let httpURLResponse = httpURLResponse,
|
|
|
|
|
isResponseRetryable(httpURLResponse) else { return false }
|
2024-07-23 12:41:16 -05:00
|
|
|
|
|
|
|
|
// At this point, retryCount hasn't been incremented yet, so we'll need to do it early here
|
|
|
|
|
// to determine if another retry is appropriate.
|
|
|
|
|
let nextRetryCount = request.retryCount + 1
|
|
|
|
|
|
|
|
|
|
guard nextRetryCount <= self.retryBackoffIntervals.count else {
|
|
|
|
|
Logger.error(
|
|
|
|
|
NetworkStrings.api_request_failed_all_retries(
|
|
|
|
|
httpMethod: request.method.httpMethod,
|
|
|
|
|
path: request.path,
|
|
|
|
|
retryCount: request.retryCount
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let retryBackoffInterval: TimeInterval = calculateRetryBackoffTime(
|
|
|
|
|
forResponse: httpURLResponse,
|
|
|
|
|
retryCount: nextRetryCount
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
Logger.debug(
|
|
|
|
|
NetworkStrings.api_request_queued_for_retry(
|
|
|
|
|
httpMethod: request.method.httpMethod,
|
|
|
|
|
retryNumber: nextRetryCount,
|
|
|
|
|
path: request.path,
|
|
|
|
|
backoffInterval: retryBackoffInterval
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
self.operationDispatcher.dispatchOnWorkerThread(after: retryBackoffInterval) {
|
|
|
|
|
let retriedRequest = request.retriedRequest()
|
|
|
|
|
self.state.modify {
|
|
|
|
|
$0.queuedRequests.insert(retriedRequest, at: 0)
|
|
|
|
|
}
|
|
|
|
|
self.beginNextRequest()
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-25 16:46:31 -05:00
|
|
|
internal func isResponseRetryable(_ urlResponse: HTTPURLResponse) -> Bool {
|
2024-07-23 12:41:16 -05:00
|
|
|
let isStatusCodeRetryable = self.retriableStatusCodes.contains(urlResponse.httpStatusCode)
|
2024-07-25 16:46:31 -05:00
|
|
|
let doesServerAllowRetryString = urlResponse.value(forHTTPHeaderField: ResponseHeader.isRetryable.rawValue)
|
|
|
|
|
let doesServerAllowRetry: Bool
|
|
|
|
|
if let doesServerAllowRetryString = doesServerAllowRetryString {
|
|
|
|
|
doesServerAllowRetry = Bool(doesServerAllowRetryString.lowercased()) ?? true
|
2024-07-23 12:41:16 -05:00
|
|
|
} else {
|
2024-07-25 16:46:31 -05:00
|
|
|
doesServerAllowRetry = true
|
2024-07-23 12:41:16 -05:00
|
|
|
}
|
|
|
|
|
|
2024-07-25 16:46:31 -05:00
|
|
|
return isStatusCodeRetryable && doesServerAllowRetry
|
2024-07-23 12:41:16 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal func calculateRetryBackoffTime(
|
|
|
|
|
forResponse httpURLResponse: HTTPURLResponse,
|
|
|
|
|
retryCount: UInt
|
|
|
|
|
) -> TimeInterval {
|
|
|
|
|
// Use the retry after value from the backend if present
|
|
|
|
|
if let retryAfterHeaderValue = httpURLResponse.allHeaderFields[ResponseHeader.retryAfter.rawValue] as? String,
|
|
|
|
|
let retryAfterSeconds = Double(retryAfterHeaderValue) {
|
|
|
|
|
|
|
|
|
|
// Ensure that the retry interval is not negative or greater than 1 hour
|
|
|
|
|
let nonNegativeRetryAfterSeconds = max(0, retryAfterSeconds)
|
|
|
|
|
let cappedRetryInterval = min(
|
|
|
|
|
nonNegativeRetryAfterSeconds,
|
|
|
|
|
3_600 // 1 hour in seconds
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return TimeInterval(cappedRetryInterval)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Otherwise, use a default value
|
|
|
|
|
let backoffIntervalIndex = Int(max(retryCount - 1, 0))
|
|
|
|
|
let backoffIntervalIndexIsWithinBounds = backoffIntervalIndex < self.retryBackoffIntervals.count
|
|
|
|
|
return backoffIntervalIndexIsWithinBounds ? self.retryBackoffIntervals[backoffIntervalIndex] : 0
|
|
|
|
|
}
|
2021-07-30 23:03:25 +02:00
|
|
|
}
|
2022-03-04 13:42:23 -08:00
|
|
|
|
2022-08-31 13:30:38 -07:00
|
|
|
// MARK: - Extensions
|
|
|
|
|
|
2023-10-25 20:24:23 -07:00
|
|
|
extension HTTPClient {
|
|
|
|
|
|
|
|
|
|
/// Information from a response to help identify a request.
|
|
|
|
|
struct ResponseMetadata {
|
|
|
|
|
var requestID: String?
|
|
|
|
|
var amazonTraceID: String?
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-16 10:27:04 -07:00
|
|
|
extension HTTPRequest {
|
2023-02-08 09:05:20 -08:00
|
|
|
|
2023-02-22 09:20:25 -08:00
|
|
|
func requestAddingNonceIfRequired(
|
|
|
|
|
with verificationMode: Signing.ResponseVerificationMode
|
|
|
|
|
) -> HTTPRequest {
|
|
|
|
|
var result = self
|
|
|
|
|
|
|
|
|
|
if result.nonce == nil,
|
2023-07-06 10:08:45 -07:00
|
|
|
result.path.needsNonceForSigning,
|
2024-07-01 12:37:21 -07:00
|
|
|
verificationMode.isEnabled {
|
2023-02-22 09:20:25 -08:00
|
|
|
result.addRandomNonce()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-13 07:45:16 -07:00
|
|
|
func headers(
|
|
|
|
|
with authHeaders: HTTPClient.RequestHeaders,
|
2023-11-17 12:19:49 -08:00
|
|
|
defaultHeaders: HTTPClient.RequestHeaders,
|
2023-12-21 12:47:34 -08:00
|
|
|
verificationMode: Signing.ResponseVerificationMode,
|
|
|
|
|
internalSettings: InternalDangerousSettingsType
|
2023-07-13 07:45:16 -07:00
|
|
|
) -> HTTPClient.RequestHeaders {
|
2023-11-17 12:19:49 -08:00
|
|
|
var result: HTTPClient.RequestHeaders = defaultHeaders
|
2023-02-08 09:05:20 -08:00
|
|
|
|
|
|
|
|
if self.path.authenticated {
|
|
|
|
|
result += authHeaders
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let nonce = self.nonce {
|
|
|
|
|
result += HTTPClient.nonceHeader(with: nonce)
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-01 12:37:21 -07:00
|
|
|
if verificationMode.isEnabled,
|
2023-12-21 11:53:10 -08:00
|
|
|
self.path.supportsSignatureVerification {
|
2023-12-21 12:47:34 -08:00
|
|
|
let headerParametersSignature = HTTPClient.headerParametersForSignatureHeader(
|
2023-12-21 11:53:10 -08:00
|
|
|
with: defaultHeaders,
|
|
|
|
|
path: self.path
|
|
|
|
|
)
|
|
|
|
|
|
2023-12-21 12:47:34 -08:00
|
|
|
#if DEBUG
|
|
|
|
|
if !internalSettings.disableHeaderSignatureVerification {
|
|
|
|
|
result += headerParametersSignature
|
|
|
|
|
}
|
|
|
|
|
#else
|
|
|
|
|
result += headerParametersSignature
|
|
|
|
|
#endif
|
|
|
|
|
|
2023-12-21 11:53:10 -08:00
|
|
|
if let body = self.requestBody {
|
2023-11-17 12:19:49 -08:00
|
|
|
result += HTTPClient.postParametersHeaderForSigning(with: body)
|
|
|
|
|
}
|
2023-12-21 11:53:10 -08:00
|
|
|
}
|
2023-07-13 07:45:16 -07:00
|
|
|
|
2023-02-08 09:05:20 -08:00
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-10 15:23:13 -07:00
|
|
|
/// Add a nonce to the request
|
|
|
|
|
private mutating func addRandomNonce() {
|
|
|
|
|
self.nonce = Data.randomNonce()
|
2022-03-04 13:42:23 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
2022-03-08 12:56:13 -08:00
|
|
|
|
2022-04-21 09:31:20 -07:00
|
|
|
private extension NetworkError {
|
|
|
|
|
|
|
|
|
|
/// Creates a `NetworkError` from any request `Error`.
|
|
|
|
|
init(_ error: Error, dnsChecker: DNSCheckerType.Type) {
|
2022-12-21 08:25:23 -08:00
|
|
|
if let blockedError = dnsChecker.errorWithBlockedHostFromError(error) {
|
2022-04-21 09:31:20 -07:00
|
|
|
Logger.error(blockedError.description)
|
|
|
|
|
self = blockedError
|
|
|
|
|
} else {
|
2023-10-25 20:25:09 -07:00
|
|
|
self = .networkError(error as NSError)
|
2022-04-21 09:31:20 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-30 12:24:53 -07:00
|
|
|
extension Result where Success == Data?, Failure == NetworkError {
|
2022-04-12 15:11:22 -07:00
|
|
|
|
2023-06-30 12:24:53 -07:00
|
|
|
/// Converts a `Result<Data?, NetworkError>` into `Result<HTTPResponse<Data?>, NetworkError>`
|
|
|
|
|
func mapToResponse(
|
|
|
|
|
response: HTTPURLResponse,
|
|
|
|
|
request: HTTPRequest
|
|
|
|
|
) -> Result<HTTPResponse<Data?>, Failure> {
|
|
|
|
|
return self.flatMap { body in
|
|
|
|
|
return .success(
|
|
|
|
|
.init(
|
2023-10-25 20:24:23 -07:00
|
|
|
httpStatusCode: response.httpStatusCode,
|
2023-06-30 12:24:53 -07:00
|
|
|
responseHeaders: response.allHeaderFields,
|
|
|
|
|
body: body
|
|
|
|
|
)
|
|
|
|
|
)
|
2022-04-12 15:11:22 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
2023-03-01 09:24:32 -08:00
|
|
|
|
2023-06-30 12:24:53 -07:00
|
|
|
extension Result where Success == VerifiedHTTPResponse<Data>, Failure == NetworkError {
|
|
|
|
|
|
|
|
|
|
// Parses a `Result<VerifiedHTTPResponse<Data>>` to `Result<VerifiedHTTPResponse<Value>>`
|
|
|
|
|
func parseResponse<Value: HTTPResponseBody>() -> VerifiedHTTPResponse<Value>.Result {
|
|
|
|
|
return self.flatMap { response in // Convert the `Result` type
|
|
|
|
|
Result<VerifiedHTTPResponse<Value>, Error> { // Create a new `Result<Value>`
|
|
|
|
|
try response.mapBody { data in // Convert the from `Data` -> `Value`
|
|
|
|
|
try Value.create(with: data) // Decode `Data` into `Value`
|
|
|
|
|
}
|
|
|
|
|
.copyWithNewRequestDate() // Update request date for 304 responses
|
|
|
|
|
}
|
|
|
|
|
// Convert decoding errors into `NetworkError.decoding`
|
|
|
|
|
.mapError { NetworkError.decoding($0, response.response.body) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
extension Result where Success == VerifiedHTTPResponse<Data>, Failure == NetworkError {
|
|
|
|
|
|
|
|
|
|
// Converts an unsuccessful response into a `Result.failure`
|
|
|
|
|
fileprivate func convertUnsuccessfulResponseToError() -> Self {
|
|
|
|
|
return self.flatMap {
|
2023-10-25 20:24:23 -07:00
|
|
|
$0.response.httpStatusCode.isSuccessfulResponse
|
2023-06-30 12:24:53 -07:00
|
|
|
? .success($0)
|
|
|
|
|
: .failure($0.response.parseUnsuccessfulResponse())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private extension VerifiedHTTPResponse {
|
2023-03-01 09:24:32 -08:00
|
|
|
|
2023-05-24 09:51:50 -07:00
|
|
|
func copyWithNewRequestDate() -> Self {
|
2023-03-01 09:24:32 -08:00
|
|
|
// Update request time from server unless it failed verification.
|
|
|
|
|
guard self.verificationResult != .failed, let requestDate = self.requestDate else { return self }
|
|
|
|
|
|
|
|
|
|
return self.mapBody {
|
|
|
|
|
return $0.copy(with: requestDate)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-25 20:24:23 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private extension HTTPResponseType {
|
|
|
|
|
|
2023-06-19 08:16:57 -07:00
|
|
|
var isLoadShedder: Bool {
|
2023-10-25 20:24:23 -07:00
|
|
|
return self.value(forHeaderField: HTTPClient.ResponseHeader.isLoadShedder) == "true"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var metadata: HTTPClient.ResponseMetadata {
|
|
|
|
|
return .init(
|
|
|
|
|
requestID: self.value(forHeaderField: HTTPClient.ResponseHeader.requestID),
|
|
|
|
|
amazonTraceID: self.value(forHeaderField: HTTPClient.ResponseHeader.amazonTraceID)
|
|
|
|
|
)
|
2023-06-19 08:16:57 -07:00
|
|
|
}
|
|
|
|
|
|
2023-03-01 09:24:32 -08:00
|
|
|
}
|
2023-05-24 09:51:50 -07:00
|
|
|
|
|
|
|
|
private extension HTTPResponse where Body == Data {
|
|
|
|
|
|
|
|
|
|
func parseUnsuccessfulResponse() -> NetworkError {
|
2023-08-17 09:45:22 -07:00
|
|
|
let contentType = self.value(forHeaderField: HTTPClient.ResponseHeader.contentType) ?? ""
|
|
|
|
|
let isJSON = contentType.starts(with: "application/json")
|
2023-05-24 09:51:50 -07:00
|
|
|
|
|
|
|
|
return .errorResponse(
|
|
|
|
|
isJSON
|
|
|
|
|
? .from(self.body)
|
|
|
|
|
: .defaultResponse,
|
2023-10-25 20:24:23 -07:00
|
|
|
self.httpStatusCode
|
2023-05-24 09:51:50 -07:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|