@@ -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//
237377extension 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