-
Notifications
You must be signed in to change notification settings - Fork 16
/
RichPushHttpClient.swift
196 lines (167 loc) · 7.17 KB
/
RichPushHttpClient.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
import CioInternalCommon
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
// sourcery: InjectRegisterShared = "HttpClient"
public class RichPushHttpClient: HttpClient {
private let httpRequestRunner: HttpRequestRunner
private let jsonAdapter: JsonAdapter
private let logger: Logger
private let cioApiSession: URLSession // only used to call the CIO API.
private let publicSession: URLSession // session used to call servers accessible to the public (such as CDNs)
private var allSessions: [URLSession] {
[cioApiSession, publicSession]
}
public func request(_ params: CioInternalCommon.HttpRequestParams, onComplete: @escaping (Result<Data, CioInternalCommon.HttpRequestError>) -> Void) {
httpRequestRunner
.request(
params: params,
session: getSessionForRequest(url: params.url)
) { [weak self] data, response, error in
guard let self = self else { return }
if let error = error {
logger.error("Error sending request \(error.localizedDescription).")
if let error = self.isUrlError(error) {
return onComplete(.failure(error))
}
return onComplete(.failure(.noRequestMade(error)))
} else if let httpResponse = response {
if httpResponse.statusCode < 300 {
guard let data = data else {
return onComplete(.failure(.noRequestMade(nil)))
}
onComplete(.success(data))
} else {
logger.error("""
\(httpResponse.statusCode) HTTP status code response.
Error description: \(httpResponse.description)
""")
let unsuccessfulStatusCodeError: HttpRequestError =
.unsuccessfulStatusCode(
httpResponse.statusCode,
apiMessage: getErrorMessageFromServerResponse(responseBody: data)
)
onComplete(.failure(unsuccessfulStatusCodeError))
}
} else {
onComplete(.failure(.noRequestMade(nil)))
}
}
}
func getSessionForRequest(url: URL) -> URLSession {
let cioApiHostname = URL(string: Self.defaultAPIHost)!.host
let requestHostname = url.host
let isRequestToCIOApi = cioApiHostname == requestHostname
return isRequestToCIOApi ? cioApiSession : publicSession
}
private func getErrorMessageFromServerResponse(responseBody: Data?) -> String {
guard let data = responseBody, var errorBodyString = data.string else {
return "(server did not give a response)"
}
// don't log errors for JSON mapping since we are trying to decode *multiple* error classes.
// we are bound to fail more often and don't want to log errors that are not super helpful to us.
if let errorMessageBody: ErrorMessageResponse = jsonAdapter.fromJson(
data,
logErrors: false
) {
errorBodyString = errorMessageBody.meta.error
} else if let errorMessageBody: ErrorsMessageResponse = jsonAdapter.fromJson(
data,
logErrors: false
) {
errorBodyString = errorMessageBody.meta.errors.joined(separator: ",")
}
return errorBodyString
}
private func isUrlError(_ error: Error) -> HttpRequestError? {
guard let urlError = error as? URLError else { return nil }
switch urlError.code {
case .notConnectedToInternet, .networkConnectionLost, .timedOut:
return .noOrBadNetwork(urlError)
case .cancelled:
return .cancelled
default: return nil
}
}
public func downloadFile(url: URL, fileType: DownloadFileType, onComplete: @escaping (URL?) -> Void) {
httpRequestRunner.downloadFile(
url: url,
fileType: fileType,
session: getSessionForRequest(url: url),
onComplete: onComplete
)
}
public func cancel(finishTasks: Bool) {
if finishTasks {
allSessions.forEach { $0.finishTasksAndInvalidate() }
} else {
allSessions.forEach { $0.invalidateAndCancel() }
}
}
init(
jsonAdapter: JsonAdapter,
httpRequestRunner: HttpRequestRunner,
logger: Logger,
deviceInfo: DeviceInfo
) {
self.httpRequestRunner = httpRequestRunner
self.jsonAdapter = jsonAdapter
self.logger = logger
self.publicSession = Self.getBasicSession()
self.cioApiSession = Self.getCIOApiSession(
key: MessagingPush.moduleConfig.writeKey,
userAgentHeaderValue: deviceInfo.getUserAgentHeaderValue()
)
}
deinit {
self.cancel(finishTasks: true)
}
}
extension RichPushHttpClient {
public static let defaultAPIHost = "https://cdp.customer.io/v1"
static func authorizationHeaderForWriteKey(_ key: String) -> String {
var returnHeader = "\(key):"
if let encodedRawHeader = returnHeader.data(using: .utf8) {
returnHeader = encodedRawHeader.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
}
return returnHeader
}
static func getCIOsessionHeader(
writeKey: String,
userAgentHeaderValue: String
) -> [String: String] {
let basicAuthHeaderString = "Basic \(authorizationHeaderForWriteKey(writeKey))"
return ["Content-Type": "application/json; charset=utf-8",
"User-Agent": userAgentHeaderValue,
"Authorization": basicAuthHeaderString]
}
static func getBasicSession() -> URLSession {
let configuration = URLSessionConfiguration.ephemeral
configuration.allowsCellularAccess = true
configuration.timeoutIntervalForResource = 30
configuration.timeoutIntervalForRequest = 60
configuration.httpAdditionalHeaders = [:]
let session = URLSession(configuration: configuration, delegate: nil, delegateQueue: nil)
return session
}
static func getCIOApiSession(key: String, userAgentHeaderValue: String) -> URLSession {
let urlSessionConfig = getBasicSession().configuration
urlSessionConfig.httpAdditionalHeaders = getCIOsessionHeader(writeKey: key, userAgentHeaderValue: userAgentHeaderValue)
return URLSession(configuration: urlSessionConfig, delegate: nil, delegateQueue: nil)
}
}
extension DeviceInfo {
func getUserAgentHeaderValue() -> String {
var userAgent = "Customer.io NSE Client/\(sdkVersion)"
// Append device details if available
if let deviceModel = deviceModel,
let osName = osName,
let osVersion = osVersion {
userAgent += " (\(deviceModel); \(osName) \(osVersion))"
}
// App details
userAgent += " \(customerBundleId)/\(customerAppVersion)"
return userAgent
}
}