Skip to content

Commit c66a7c0

Browse files
committed
Move error handling to separate file and make it thread safe
1 parent 6e4f812 commit c66a7c0

File tree

3 files changed

+621
-131
lines changed

3 files changed

+621
-131
lines changed

Modules/Sources/NetworkingCore/Network/AlamofireNetwork.swift

Lines changed: 12 additions & 131 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,139 +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-
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
//
408304
extension 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

430311
private extension DataRequest {
431312
/// Validates only for `RESTRequest`
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import Foundation
2+
import Alamofire
3+
4+
/// Thread-safe handler for network error tracking and retry logic
5+
final class AlamofireNetworkErrorHandler {
6+
private let queue = DispatchQueue(label: "com.networkingcore.errorhandler", attributes: .concurrent)
7+
private let userDefaults: UserDefaults
8+
private let credentials: Credentials?
9+
10+
private var _appPasswordFailures: [Int64: Int] = [:]
11+
private var _retriedJetpackRequests: [RetriedJetpackRequest] = []
12+
13+
init(credentials: Credentials?, userDefaults: UserDefaults = .standard) {
14+
self.credentials = credentials
15+
self.userDefaults = userDefaults
16+
}
17+
18+
// MARK: - Thread-safe property access
19+
20+
private var appPasswordFailures: [Int64: Int] {
21+
get {
22+
queue.sync { _appPasswordFailures }
23+
}
24+
set {
25+
queue.sync(flags: .barrier) { [weak self] in
26+
self?._appPasswordFailures = newValue
27+
}
28+
}
29+
}
30+
31+
private var retriedJetpackRequests: [RetriedJetpackRequest] {
32+
get {
33+
queue.sync { _retriedJetpackRequests }
34+
}
35+
set {
36+
queue.sync(flags: .barrier) { [weak self] in
37+
self?._retriedJetpackRequests = newValue
38+
}
39+
}
40+
}
41+
42+
// MARK: - Public interface
43+
44+
func resetFailureCount(for siteID: Int64) {
45+
appPasswordFailures.removeValue(forKey: siteID)
46+
}
47+
48+
func shouldRetryJetpackRequest(originalRequest: URLRequestConvertible,
49+
convertedRequest: URLRequestConvertible,
50+
failure: Error?) -> Bool {
51+
guard let error = failure,
52+
let request = originalRequest as? JetpackRequest,
53+
convertedRequest is RESTRequest,
54+
case .some(.wpcom) = self.credentials else {
55+
return false
56+
}
57+
58+
let isExpectedError: Bool = {
59+
switch error {
60+
case AFError.requestAdaptationFailed:
61+
return true
62+
case _ as NetworkError:
63+
return true
64+
default:
65+
return false
66+
}
67+
}()
68+
69+
if isExpectedError {
70+
let retriedRequest = RetriedJetpackRequest(request: request, error: error)
71+
retriedJetpackRequests.append(retriedRequest)
72+
return true
73+
}
74+
return false
75+
}
76+
77+
func flagSiteAsUnsupportedForAppPasswordIfNeeded(
78+
originalRequest: URLRequestConvertible,
79+
failure: Error?
80+
) {
81+
let retriedRequestIndex = retriedJetpackRequests.firstIndex { retriedRequest in
82+
let urlRequest = try? originalRequest.asURLRequest()
83+
let retriedRequest = try? retriedRequest.request.asURLRequest()
84+
return urlRequest == retriedRequest
85+
}
86+
87+
guard let index = retriedRequestIndex else { return }
88+
89+
let retriedRequest = retriedJetpackRequests[index]
90+
91+
if failure == nil {
92+
let siteID = retriedRequest.request.siteID
93+
let originalFailure = retriedRequest.error
94+
switch originalFailure {
95+
case NetworkError.unacceptableStatusCode(statusCode: 401, _),
96+
NetworkError.unacceptableStatusCode(statusCode: 403, _),
97+
NetworkError.unacceptableStatusCode(statusCode: 429, _):
98+
flagSiteAsUnsupported(for: siteID)
99+
default:
100+
if let networkError = originalFailure as? NetworkError,
101+
let code = networkError.errorCode,
102+
AppPasswordConstants.disabledCodes.contains(code) {
103+
flagSiteAsUnsupported(for: siteID)
104+
} else {
105+
incrementFailureCount(for: siteID)
106+
}
107+
}
108+
}
109+
110+
retriedJetpackRequests.remove(at: index)
111+
}
112+
113+
func handleFailureForDirectRequestIfNeeded(originalRequest: URLRequestConvertible,
114+
convertedRequest: URLRequestConvertible,
115+
failure: Error?,
116+
onRetry: @escaping () -> Void,
117+
onCompletion: @escaping () -> Void) {
118+
if shouldRetryJetpackRequest(originalRequest: originalRequest,
119+
convertedRequest: convertedRequest,
120+
failure: failure) {
121+
onRetry()
122+
} else {
123+
flagSiteAsUnsupportedForAppPasswordIfNeeded(originalRequest: originalRequest, failure: failure)
124+
onCompletion()
125+
}
126+
}
127+
128+
func isRequestRetried(_ request: URLRequestConvertible) -> Bool {
129+
retriedJetpackRequests.contains { retriedRequest in
130+
let urlRequest = try? request.asURLRequest()
131+
let currentItem = try? retriedRequest.request.asURLRequest()
132+
return currentItem == urlRequest
133+
}
134+
}
135+
136+
func flagSiteAsUnsupported(for siteID: Int64) {
137+
queue.sync(flags: .barrier) {
138+
let currentList = userDefaults.applicationPasswordUnsupportedList
139+
userDefaults.applicationPasswordUnsupportedList = currentList + [siteID]
140+
}
141+
}
142+
143+
// MARK: - Private methods
144+
145+
private func incrementFailureCount(for siteID: Int64) {
146+
let currentFailureCount = appPasswordFailures[siteID] ?? 0
147+
let updatedCount = currentFailureCount + 1
148+
if updatedCount == AppPasswordConstants.requestFailureThreshold {
149+
flagSiteAsUnsupported(for: siteID)
150+
}
151+
appPasswordFailures[siteID] = updatedCount
152+
}
153+
}
154+
155+
/// Helper type to keep track of retried requests with accompanied error
156+
struct RetriedJetpackRequest {
157+
let request: JetpackRequest
158+
let error: Error
159+
}
160+
161+
// MARK: - Constants for direct request error handling
162+
enum AppPasswordConstants {
163+
static let requestFailureThreshold = 10
164+
static let disabledCodes = [
165+
"application_passwords_disabled",
166+
"application_passwords_disabled_for_user",
167+
"incorrect_password"
168+
]
169+
}

0 commit comments

Comments
 (0)