Skip to content

Commit 328b3bc

Browse files
committed
Update error handling for direct requests
1 parent d5786ad commit 328b3bc

File tree

1 file changed

+188
-39
lines changed

1 file changed

+188
-39
lines changed

Modules/Sources/NetworkingCore/Network/AlamofireNetwork.swift

Lines changed: 188 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ public class AlamofireNetwork: Network {
5252
/// Keeps track of failure counts for each site when switching to direct requests
5353
private var appPasswordFailures: [Int64: Int] = [:]
5454

55+
/// Keeps track of retried requests when direct requests fail
56+
private var retriedJetpackRequests: [RetriedJetpackRequest] = []
57+
5558
/// Public Initializer
5659
///
5760
/// - Parameters:
@@ -116,11 +119,21 @@ public class AlamofireNetwork: Network {
116119
/// - Yes. We do the above because the Jetpack Tunnel endpoint doesn't properly relay the correct statusCode.
117120
///
118121
public func responseData(for request: URLRequestConvertible, completion: @escaping (Data?, Error?) -> Void) {
119-
let request = requestConverter.convert(request)
120-
alamofireSession.request(request)
121-
.validateIfRestRequest(for: request)
122-
.responseData { response in
123-
completion(response.value, response.networkingError)
122+
let convertedRequest = convertRequestIfNeeded(request)
123+
alamofireSession.request(convertedRequest)
124+
.validateIfRestRequest(for: convertedRequest)
125+
.responseData { [weak self] response in
126+
self?.handleFailureForDirectRequestIfNeeded(
127+
originalRequest: request,
128+
convertedRequest: convertedRequest,
129+
failure: response.networkingError,
130+
onRetry: {
131+
self?.responseData(for: request, completion: completion)
132+
},
133+
onCompletion: {
134+
completion(response.value, response.networkingError)
135+
}
136+
)
124137
}
125138
}
126139

@@ -134,23 +147,45 @@ public class AlamofireNetwork: Network {
134147
/// - completion: Closure to be executed upon completion.
135148
///
136149
public func responseData(for request: URLRequestConvertible, completion: @escaping (Swift.Result<Data, Error>) -> Void) {
137-
let request = requestConverter.convert(request)
138-
alamofireSession.request(request)
139-
.validateIfRestRequest(for: request)
140-
.responseData { response in
141-
if let error = response.networkingError {
142-
completion(.failure(error))
143-
} else {
144-
completion(response.result.mapError { $0 })
145-
}
150+
let convertedRequest = convertRequestIfNeeded(request)
151+
alamofireSession.request(convertedRequest)
152+
.validateIfRestRequest(for: convertedRequest)
153+
.responseData { [weak self] response in
154+
self?.handleFailureForDirectRequestIfNeeded(
155+
originalRequest: request,
156+
convertedRequest: convertedRequest,
157+
failure: response.networkingError,
158+
onRetry: {
159+
self?.responseData(for: request, completion: completion)
160+
},
161+
onCompletion: {
162+
if let error = response.networkingError {
163+
completion(.failure(error))
164+
} else {
165+
completion(response.result.mapError { $0 })
166+
}
167+
}
168+
)
146169
}
147170
}
148171

149172
public func responseDataAndHeaders(for request: URLRequestConvertible) async throws -> (Data, ResponseHeaders?) {
150-
let request = requestConverter.convert(request)
151-
let sessionRequest = alamofireSession.request(request)
152-
.validateIfRestRequest(for: request)
173+
let convertedRequest = convertRequestIfNeeded(request)
174+
let sessionRequest = alamofireSession.request(convertedRequest)
175+
.validateIfRestRequest(for: convertedRequest)
153176
let response = await sessionRequest.serializingData().response
177+
let failure = response.networkingError
178+
179+
if shouldRetryJetpackRequest(
180+
originalRequest: request,
181+
convertedRequest: convertedRequest,
182+
failure: failure
183+
) {
184+
return try await responseDataAndHeaders(for: request)
185+
}
186+
187+
flagSiteAsUnsupportedForAppPasswordIfNeeded(originalRequest: request, failure: failure)
188+
154189
if let error = response.networkingError {
155190
throw error
156191
}
@@ -172,28 +207,50 @@ public class AlamofireNetwork: Network {
172207
/// - Returns: A publisher that emits the result of the given request.
173208
public func responseDataPublisher(for request: URLRequestConvertible) -> AnyPublisher<Swift.Result<Data, Error>, Never> {
174209
return Future() { promise in
175-
let request = self.requestConverter.convert(request)
210+
let convertedRequest = self.convertRequestIfNeeded(request)
176211
self.alamofireSession
177-
.request(request)
178-
.validateIfRestRequest(for: request)
179-
.responseData { response in
180-
if let error = response.networkingError {
181-
promise(.success(.failure(error)))
182-
} else {
183-
promise(.success(response.result.mapError { $0 }))
184-
}
212+
.request(convertedRequest)
213+
.validateIfRestRequest(for: convertedRequest)
214+
.responseData { [weak self] response in
215+
self?.handleFailureForDirectRequestIfNeeded(
216+
originalRequest: request,
217+
convertedRequest: convertedRequest,
218+
failure: response.networkingError,
219+
onRetry: {
220+
self?.responseData(for: request) { result in
221+
promise(.success(result))
222+
}
223+
},
224+
onCompletion: {
225+
if let error = response.networkingError {
226+
promise(.success(.failure(error)))
227+
} else {
228+
promise(.success(response.result.mapError { $0 }))
229+
}
230+
}
231+
)
185232
}
186233
}.eraseToAnyPublisher()
187234
}
188235

189236
public func uploadMultipartFormData(multipartFormData: @escaping (MultipartFormData) -> Void,
190237
to request: URLRequestConvertible,
191238
completion: @escaping (Data?, Error?) -> Void) {
192-
let request = requestConverter.convert(request)
239+
let convertedRequest = self.convertRequestIfNeeded(request)
193240
alamofireSession
194-
.upload(multipartFormData: multipartFormData, with: request)
195-
.responseData { response in
196-
completion(response.value, response.error)
241+
.upload(multipartFormData: multipartFormData, with: convertedRequest)
242+
.responseData { [weak self] response in
243+
self?.handleFailureForDirectRequestIfNeeded(
244+
originalRequest: request,
245+
convertedRequest: convertedRequest,
246+
failure: response.networkingError,
247+
onRetry: {
248+
self?.uploadMultipartFormData(multipartFormData: multipartFormData, to: request, completion: completion)
249+
},
250+
onCompletion: {
251+
completion(response.value, response.error)
252+
}
253+
)
197254
}
198255
}
199256
}
@@ -232,23 +289,114 @@ private extension AlamofireNetwork {
232289
}
233290
}
234291

292+
// MARK: Helper methods for error handling
293+
//
294+
private extension AlamofireNetwork {
295+
func convertRequestIfNeeded(_ request: URLRequestConvertible) -> URLRequestConvertible {
296+
let isRetried = retriedJetpackRequests.contains { retriedRequest in
297+
let urlRequest = try? request.asURLRequest()
298+
let currentItem = try? retriedRequest.request.asURLRequest()
299+
return currentItem == urlRequest
300+
}
301+
if isRetried {
302+
return request // do not convert
303+
}
304+
return requestConverter.convert(request)
305+
}
306+
307+
/// Checks if the specified request and error are eligible for retrying as Jetpack request.
308+
/// If yes, enqueue the original request to the retried list before returning.
309+
///
310+
func shouldRetryJetpackRequest(originalRequest: URLRequestConvertible,
311+
convertedRequest: URLRequestConvertible,
312+
failure: Error?) -> Bool {
313+
if let request = originalRequest as? JetpackRequest,
314+
convertedRequest is RESTRequest,
315+
case .some(.wpcom) = credentials,
316+
let failure = failure as? NetworkError {
317+
let retriedRequest = RetriedJetpackRequest(request: request, error: failure)
318+
retriedJetpackRequests.append(retriedRequest)
319+
return true
320+
}
321+
return false
322+
}
323+
324+
/// Determines if the site has issue with application password based on the original request.
325+
///
326+
func flagSiteAsUnsupportedForAppPasswordIfNeeded(originalRequest: URLRequestConvertible,
327+
failure: Error?) {
328+
let retriedRequestIndex = retriedJetpackRequests.firstIndex { retriedRequest in
329+
let urlRequest = try? originalRequest.asURLRequest()
330+
let retriedRequest = try? retriedRequest.request.asURLRequest()
331+
return urlRequest == retriedRequest
332+
}
333+
334+
if failure == nil, let index = retriedRequestIndex {
335+
let siteID = retriedJetpackRequests[index].request.siteID
336+
let originalFailure = retriedJetpackRequests[index].error
337+
switch originalFailure {
338+
case .unacceptableStatusCode(statusCode: 401, _),
339+
.unacceptableStatusCode(statusCode: 403, _),
340+
.unacceptableStatusCode(statusCode: 429, _):
341+
flagSiteAsUnsupported(for: siteID)
342+
default:
343+
if let code = originalFailure.errorCode, AppPasswordConstants.disabledCodes.contains(code) {
344+
flagSiteAsUnsupported(for: siteID)
345+
} else {
346+
incrementFailureCount(for: siteID)
347+
}
348+
}
349+
}
350+
}
351+
352+
func handleFailureForDirectRequestIfNeeded(originalRequest: URLRequestConvertible,
353+
convertedRequest: URLRequestConvertible,
354+
failure: Error?,
355+
onRetry: @escaping () -> Void,
356+
onCompletion: @escaping () -> Void) {
357+
if shouldRetryJetpackRequest(originalRequest: originalRequest,
358+
convertedRequest: convertedRequest,
359+
failure: failure) {
360+
onRetry()
361+
} else {
362+
flagSiteAsUnsupportedForAppPasswordIfNeeded(originalRequest: originalRequest, failure: failure)
363+
onCompletion()
364+
}
365+
}
366+
367+
/// Helper type to keep track of retried requests with accompanied error
368+
///
369+
struct RetriedJetpackRequest {
370+
let request: JetpackRequest
371+
let error: NetworkError
372+
}
373+
}
374+
235375
// MARK: `RequestProcessorDelegate` conformance
236376
//
237377
extension AlamofireNetwork: RequestProcessorDelegate {
238378
func didFailToAuthenticateRequestWithAppPassword(siteID: Int64, reason: AppPasswordFailureReason) {
239379
switch reason {
240380
case .notSupported:
381+
flagSiteAsUnsupported(for: siteID)
382+
case .unknown:
383+
incrementFailureCount(for: siteID)
384+
}
385+
}
386+
387+
func flagSiteAsUnsupported(for siteID: Int64) {
388+
let currentList = userDefaults.applicationPasswordUnsupportedList
389+
userDefaults.applicationPasswordUnsupportedList = currentList + [siteID]
390+
}
391+
392+
func incrementFailureCount(for siteID: Int64) {
393+
let currentFailureCount = appPasswordFailures[siteID] ?? 0
394+
let updatedCount = currentFailureCount + 1
395+
if updatedCount == AppPasswordConstants.requestFailureThreshold {
241396
let currentList = userDefaults.applicationPasswordUnsupportedList
242397
userDefaults.applicationPasswordUnsupportedList = currentList + [siteID]
243-
case .unknown:
244-
let currentFailureCount = appPasswordFailures[siteID] ?? 0
245-
let updatedCount = currentFailureCount + 1
246-
if updatedCount == AppPasswordConstants.requestFailureThreshold {
247-
let currentList = userDefaults.applicationPasswordUnsupportedList
248-
userDefaults.applicationPasswordUnsupportedList = currentList + [siteID]
249-
}
250-
appPasswordFailures[siteID] = updatedCount
251398
}
399+
appPasswordFailures[siteID] = updatedCount
252400
}
253401
}
254402

@@ -258,7 +406,8 @@ enum AppPasswordConstants {
258406
static let requestFailureThreshold = 10
259407
static let disabledCodes = [
260408
"application_passwords_disabled",
261-
"application_passwords_disabled_for_user"
409+
"application_passwords_disabled_for_user",
410+
"incorrect_password"
262411
]
263412
}
264413

0 commit comments

Comments
 (0)