Skip to content

Commit 636c35f

Browse files
authored
Application password experiment: Fall back to Jetpack proxy after app password generation fails (#16079)
2 parents e97724f + 0af25e0 commit 636c35f

File tree

6 files changed

+645
-199
lines changed

6 files changed

+645
-199
lines changed

Modules/Sources/NetworkingCore/ApplicationPassword/RequestProcessor.swift

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
import Alamofire
22
import Foundation
33

4-
enum AppPasswordFailureReason {
5-
case notSupported
6-
case unknown
7-
}
8-
94
protocol RequestProcessorDelegate: AnyObject {
10-
func didFailToAuthenticateRequestWithAppPassword(siteID: Int64, reason: AppPasswordFailureReason)
5+
func didFailToAuthenticateRequestWithAppPassword(siteID: Int64)
116
}
127

138
/// Authenticates and retries requests
@@ -92,7 +87,7 @@ private extension RequestProcessor {
9287
isAuthenticating = false
9388
completeRequests(false)
9489
if let currentSiteID {
95-
notifyFailure(error, for: currentSiteID)
90+
notifyFailureIfNeeded(error, for: currentSiteID)
9691
}
9792
}
9893
}
@@ -120,22 +115,24 @@ private extension RequestProcessor {
120115
}
121116
}
122117

123-
func notifyFailure(_ error: Error, for siteID: Int64) {
124-
let reason: AppPasswordFailureReason = {
118+
func notifyFailureIfNeeded(_ error: Error, for siteID: Int64) {
119+
let appPasswordNotSupported: Bool = {
125120
switch error {
126121
case NetworkError.notFound:
127-
return .notSupported
122+
return true
128123
case let networkError as NetworkError:
129124
if let code = networkError.errorCode,
130125
AppPasswordConstants.disabledCodes.contains(code) {
131-
return .notSupported
126+
return true
132127
}
133-
return .unknown
128+
return false
134129
default:
135-
return .unknown
130+
return false
136131
}
137132
}()
138-
delegate?.didFailToAuthenticateRequestWithAppPassword(siteID: siteID, reason: reason)
133+
if appPasswordNotSupported {
134+
delegate?.didFailToAuthenticateRequestWithAppPassword(siteID: siteID)
135+
}
139136
}
140137

141138
func shouldRetry(_ error: Error) -> Bool {

Modules/Sources/NetworkingCore/Network/AlamofireNetwork.swift

Lines changed: 17 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -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,127 +291,22 @@ private extension AlamofireNetwork {
293291
//
294292
private 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-
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-
guard let index = retriedRequestIndex else { return }
334-
335-
if failure == nil {
336-
let siteID = retriedJetpackRequests[index].request.siteID
337-
let originalFailure = retriedJetpackRequests[index].error
338-
switch originalFailure {
339-
case .unacceptableStatusCode(statusCode: 401, _),
340-
.unacceptableStatusCode(statusCode: 403, _),
341-
.unacceptableStatusCode(statusCode: 429, _):
342-
flagSiteAsUnsupported(for: siteID)
343-
default:
344-
if let code = originalFailure.errorCode, AppPasswordConstants.disabledCodes.contains(code) {
345-
flagSiteAsUnsupported(for: siteID)
346-
} else {
347-
incrementFailureCount(for: siteID)
348-
}
349-
}
350-
}
351-
352-
// remove retried request from list
353-
retriedJetpackRequests.remove(at: index)
354-
}
355-
356-
func handleFailureForDirectRequestIfNeeded(originalRequest: URLRequestConvertible,
357-
convertedRequest: URLRequestConvertible,
358-
failure: Error?,
359-
onRetry: @escaping () -> Void,
360-
onCompletion: @escaping () -> Void) {
361-
if shouldRetryJetpackRequest(originalRequest: originalRequest,
362-
convertedRequest: convertedRequest,
363-
failure: failure) {
364-
onRetry()
365-
} else {
366-
flagSiteAsUnsupportedForAppPasswordIfNeeded(originalRequest: originalRequest, failure: failure)
367-
onCompletion()
368-
}
369-
}
370-
371-
/// Helper type to keep track of retried requests with accompanied error
372-
///
373-
struct RetriedJetpackRequest {
374-
let request: JetpackRequest
375-
let error: NetworkError
376-
}
377300
}
378301

379302
// MARK: `RequestProcessorDelegate` conformance
380303
//
381304
extension AlamofireNetwork: RequestProcessorDelegate {
382-
func didFailToAuthenticateRequestWithAppPassword(siteID: Int64, reason: AppPasswordFailureReason) {
383-
switch reason {
384-
case .notSupported:
385-
flagSiteAsUnsupported(for: siteID)
386-
case .unknown:
387-
incrementFailureCount(for: siteID)
388-
}
389-
}
390-
391-
func flagSiteAsUnsupported(for siteID: Int64) {
392-
let currentList = userDefaults.applicationPasswordUnsupportedList
393-
userDefaults.applicationPasswordUnsupportedList = currentList + [siteID]
394-
}
395-
396-
func incrementFailureCount(for siteID: Int64) {
397-
let currentFailureCount = appPasswordFailures[siteID] ?? 0
398-
let updatedCount = currentFailureCount + 1
399-
if updatedCount == AppPasswordConstants.requestFailureThreshold {
400-
let currentList = userDefaults.applicationPasswordUnsupportedList
401-
userDefaults.applicationPasswordUnsupportedList = currentList + [siteID]
402-
}
403-
appPasswordFailures[siteID] = updatedCount
305+
func didFailToAuthenticateRequestWithAppPassword(siteID: Int64) {
306+
errorHandler.flagSiteAsUnsupported(for: siteID)
404307
}
405308
}
406309

407-
// MARK: - Constants for direct request error handling
408-
enum AppPasswordConstants {
409-
// flag site as disabled after threshold is reached
410-
static let requestFailureThreshold = 10
411-
static let disabledCodes = [
412-
"application_passwords_disabled",
413-
"application_passwords_disabled_for_user",
414-
"incorrect_password"
415-
]
416-
}
417310

418311
private extension DataRequest {
419312
/// Validates only for `RESTRequest`
@@ -450,6 +343,10 @@ extension Alamofire.DataResponse {
450343
return error
451344
}
452345

346+
if case .some(AFError.requestAdaptationFailed) = error?.asAFError {
347+
return error
348+
}
349+
453350
return response.flatMap { response in
454351
NetworkError(responseData: data,
455352
statusCode: response.statusCode)

0 commit comments

Comments
 (0)