@@ -49,11 +49,8 @@ public class AlamofireNetwork: Network {
4949
5050 private var subscription : AnyCancellable ?
5151
52- /// Keeps track of failure counts for each site when switching to direct requests
53- private var appPasswordFailures : [ Int64 : Int ] = [ : ]
54-
55- /// Keeps track of retried requests when direct requests fail
56- private var retriedJetpackRequests : [ RetriedJetpackRequest ] = [ ]
52+ /// Thread-safe error handler for failure tracking and retry logic
53+ private let errorHandler : AlamofireNetworkErrorHandler
5754
5855 /// Public Initializer
5956 ///
@@ -71,6 +68,7 @@ public class AlamofireNetwork: Network {
7168 self . credentials = credentials
7269 self . selectedSite = selectedSite
7370 self . userDefaults = userDefaults
71+ self . errorHandler = AlamofireNetworkErrorHandler ( credentials: credentials, userDefaults: userDefaults)
7472 self . requestConverter = {
7573 let siteAddress : String ? = {
7674 switch credentials {
@@ -123,7 +121,7 @@ public class AlamofireNetwork: Network {
123121 alamofireSession. request ( convertedRequest)
124122 . validateIfRestRequest ( for: convertedRequest)
125123 . responseData { [ weak self] response in
126- self ? . handleFailureForDirectRequestIfNeeded (
124+ self ? . errorHandler . handleFailureForDirectRequestIfNeeded (
127125 originalRequest: request,
128126 convertedRequest: convertedRequest,
129127 failure: response. networkingError,
@@ -151,7 +149,7 @@ public class AlamofireNetwork: Network {
151149 alamofireSession. request ( convertedRequest)
152150 . validateIfRestRequest ( for: convertedRequest)
153151 . responseData { [ weak self] response in
154- self ? . handleFailureForDirectRequestIfNeeded (
152+ self ? . errorHandler . handleFailureForDirectRequestIfNeeded (
155153 originalRequest: request,
156154 convertedRequest: convertedRequest,
157155 failure: response. networkingError,
@@ -176,15 +174,15 @@ public class AlamofireNetwork: Network {
176174 let response = await sessionRequest. serializingData ( ) . response
177175 let failure = response. networkingError
178176
179- if shouldRetryJetpackRequest (
177+ if errorHandler . shouldRetryJetpackRequest (
180178 originalRequest: request,
181179 convertedRequest: convertedRequest,
182180 failure: failure
183181 ) {
184182 return try await responseDataAndHeaders ( for: request)
185183 }
186184
187- flagSiteAsUnsupportedForAppPasswordIfNeeded ( originalRequest: request, failure: failure)
185+ errorHandler . flagSiteAsUnsupportedForAppPasswordIfNeeded ( originalRequest: request, failure: failure)
188186
189187 if let error = response. networkingError {
190188 throw error
@@ -212,7 +210,7 @@ public class AlamofireNetwork: Network {
212210 . request ( convertedRequest)
213211 . validateIfRestRequest ( for: convertedRequest)
214212 . responseData { [ weak self] response in
215- self ? . handleFailureForDirectRequestIfNeeded (
213+ self ? . errorHandler . handleFailureForDirectRequestIfNeeded (
216214 originalRequest: request,
217215 convertedRequest: convertedRequest,
218216 failure: response. networkingError,
@@ -240,7 +238,7 @@ public class AlamofireNetwork: Network {
240238 alamofireSession
241239 . upload ( multipartFormData: multipartFormData, with: convertedRequest)
242240 . responseData { [ weak self] response in
243- self ? . handleFailureForDirectRequestIfNeeded (
241+ self ? . errorHandler . handleFailureForDirectRequestIfNeeded (
244242 originalRequest: request,
245243 convertedRequest: convertedRequest,
246244 failure: response. networkingError,
@@ -284,7 +282,7 @@ private extension AlamofireNetwork {
284282 network: self
285283 ) )
286284 requestAuthenticator. delegate = self
287- appPasswordFailures . removeValue ( forKey : site. siteID) // reset failure count
285+ errorHandler . resetFailureCount ( for : site. siteID) // reset failure count
288286 }
289287 }
290288}
@@ -293,139 +291,22 @@ private extension AlamofireNetwork {
293291//
294292private extension AlamofireNetwork {
295293 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 {
294+ if errorHandler. isRequestRetried ( request) {
302295 return request // do not convert
303296 }
304297 return requestConverter. convert ( request)
305298 }
306299
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- guard let error = failure,
314- let request = originalRequest as? JetpackRequest ,
315- convertedRequest is RESTRequest ,
316- case . some( . wpcom) = credentials else {
317- return false
318- }
319-
320- let isExpectedError : Bool = {
321- switch error {
322- case AFError . requestAdaptationFailed:
323- return true
324- case _ as NetworkError :
325- return true
326- default :
327- return false
328- }
329- } ( )
330-
331- if isExpectedError {
332- let retriedRequest = RetriedJetpackRequest ( request: request, error: error)
333- retriedJetpackRequests. append ( retriedRequest)
334- return true
335- }
336- return false
337- }
338-
339- /// Determines if the site has issue with application password based on the original request.
340- ///
341- func flagSiteAsUnsupportedForAppPasswordIfNeeded( originalRequest: URLRequestConvertible ,
342- failure: Error ? ) {
343- let retriedRequestIndex = retriedJetpackRequests. firstIndex { retriedRequest in
344- let urlRequest = try ? originalRequest. asURLRequest ( )
345- let retriedRequest = try ? retriedRequest. request. asURLRequest ( )
346- return urlRequest == retriedRequest
347- }
348- guard let index = retriedRequestIndex else { return }
349-
350- if failure == nil {
351- let siteID = retriedJetpackRequests [ index] . request. siteID
352- let originalFailure = retriedJetpackRequests [ index] . error
353- switch originalFailure {
354- case NetworkError . unacceptableStatusCode ( statusCode: 401 , _) ,
355- NetworkError . unacceptableStatusCode ( statusCode: 403 , _) ,
356- NetworkError . unacceptableStatusCode ( statusCode: 429 , _) :
357- flagSiteAsUnsupported ( for: siteID)
358- default :
359- if let networkError = originalFailure as? NetworkError ,
360- let code = networkError. errorCode,
361- AppPasswordConstants . disabledCodes. contains ( code) {
362- flagSiteAsUnsupported ( for: siteID)
363- } else {
364- incrementFailureCount ( for: siteID)
365- }
366- }
367- }
368-
369- // remove retried request from list
370- retriedJetpackRequests. remove ( at: index)
371- }
372-
373- func handleFailureForDirectRequestIfNeeded( originalRequest: URLRequestConvertible ,
374- convertedRequest: URLRequestConvertible ,
375- failure: Error ? ,
376- onRetry: @escaping ( ) -> Void ,
377- onCompletion: @escaping ( ) -> Void ) {
378- if shouldRetryJetpackRequest ( originalRequest: originalRequest,
379- convertedRequest: convertedRequest,
380- failure: failure) {
381- onRetry ( )
382- } else {
383- flagSiteAsUnsupportedForAppPasswordIfNeeded ( originalRequest: originalRequest, failure: failure)
384- onCompletion ( )
385- }
386- }
387-
388- /// Helper type to keep track of retried requests with accompanied error
389- ///
390- struct RetriedJetpackRequest {
391- let request : JetpackRequest
392- let error : Error
393- }
394-
395- func incrementFailureCount( for siteID: Int64 ) {
396- let currentFailureCount = appPasswordFailures [ siteID] ?? 0
397- let updatedCount = currentFailureCount + 1
398- if updatedCount == AppPasswordConstants . requestFailureThreshold {
399- let currentList = userDefaults. applicationPasswordUnsupportedList
400- userDefaults. applicationPasswordUnsupportedList = currentList + [ siteID]
401- }
402- appPasswordFailures [ siteID] = updatedCount
403- }
404300}
405301
406302// MARK: `RequestProcessorDelegate` conformance
407303//
408304extension AlamofireNetwork : RequestProcessorDelegate {
409305 func didFailToAuthenticateRequestWithAppPassword( siteID: Int64 ) {
410- flagSiteAsUnsupported ( for: siteID)
411- }
412-
413- func flagSiteAsUnsupported( for siteID: Int64 ) {
414- let currentList = userDefaults. applicationPasswordUnsupportedList
415- userDefaults. applicationPasswordUnsupportedList = currentList + [ siteID]
306+ errorHandler. flagSiteAsUnsupported ( for: siteID)
416307 }
417308}
418309
419- // MARK: - Constants for direct request error handling
420- enum AppPasswordConstants {
421- // flag site as disabled after threshold is reached
422- static let requestFailureThreshold = 10
423- static let disabledCodes = [
424- " application_passwords_disabled " ,
425- " application_passwords_disabled_for_user " ,
426- " incorrect_password "
427- ]
428- }
429310
430311private extension DataRequest {
431312 /// Validates only for `RESTRequest`
0 commit comments