Skip to content

Commit fe446dc

Browse files
authored
Merge pull request #691 from Iterable/evan/MOB-7021-retry-for-failed-requests
[MOB-7021] Implement retry mechanism to the iOS SDK for failed network requests
2 parents 84caa3d + e6566b5 commit fe446dc

File tree

2 files changed

+79
-15
lines changed

2 files changed

+79
-15
lines changed

swift-sdk/Internal/NetworkHelper.swift

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ extension NetworkError: LocalizedError {
2828
}
2929

3030
struct NetworkHelper {
31+
static let maxRetryCount = 5
32+
static let retryDelaySeconds = 2
33+
3134
static func getData(fromUrl url: URL, usingSession networkSession: NetworkSessionProtocol) -> Pending<Data, Error> {
3235
let fulfill = Fulfill<Data, Error>()
3336

@@ -52,8 +55,9 @@ struct NetworkHelper {
5255
static func sendRequest<T>(_ request: URLRequest,
5356
converter: @escaping (Data) throws -> T?,
5457
usingSession networkSession: NetworkSessionProtocol) -> Pending<T, NetworkError> {
55-
#if NETWORK_DEBUG
58+
5659
let requestId = IterableUtil.generateUUID()
60+
#if NETWORK_DEBUG
5761
print()
5862
print("====================================================>")
5963
print("sending request: \(request)")
@@ -73,29 +77,65 @@ struct NetworkHelper {
7377
#endif
7478

7579
let fulfill = Fulfill<T, NetworkError>()
76-
77-
networkSession.makeRequest(request) { data, response, error in
78-
let result = createResultFromNetworkResponse(data: data,
79-
converter: converter,
80-
response: response,
81-
error: error)
8280

83-
switch result {
84-
case let .success(value):
85-
#if NETWORK_DEBUG
86-
print("request with id: \(requestId) successfully sent, response:")
87-
print(value)
88-
#endif
89-
fulfill.resolve(with: value)
90-
case let .failure(error):
81+
func sendRequestWithRetries(request: URLRequest, requestId: String, retriesLeft: Int) {
82+
networkSession.makeRequest(request) { data, response, error in
83+
let result = createResultFromNetworkResponse(data: data,
84+
converter: converter,
85+
response: response,
86+
error: error)
87+
switch result {
88+
case let .success(value):
89+
handleSuccess(requestId: requestId, value: value)
90+
case let .failure(error):
91+
handleFailure(requestId: requestId, request: request, error: error, retriesLeft: retriesLeft)
92+
}
93+
}
94+
}
95+
96+
func handleSuccess(requestId: String, value: T) {
97+
#if NETWORK_DEBUG
98+
print("request with id: \(requestId) successfully sent, response:")
99+
print(value)
100+
#endif
101+
fulfill.resolve(with: value)
102+
}
103+
104+
func handleFailure(requestId: String, request: URLRequest, error: NetworkError, retriesLeft: Int) {
105+
if shouldRetry(error: error, retriesLeft: retriesLeft) {
106+
retryRequest(requestId: requestId, request: request, error: error, retriesLeft: retriesLeft)
107+
} else {
91108
#if NETWORK_DEBUG
92109
print("request with id: \(requestId) errored")
93110
print(error)
94111
#endif
95112
fulfill.reject(with: error)
96113
}
114+
97115
}
98116

117+
func shouldRetry(error: NetworkError, retriesLeft: Int) -> Bool {
118+
return error.httpStatusCode ?? 0 >= 500 && retriesLeft > 0
119+
}
120+
121+
func retryRequest(requestId: String, request: URLRequest, error: NetworkError, retriesLeft: Int) {
122+
#if NETWORK_DEBUG
123+
print("retry attempt: \(maxRetryCount-retriesLeft+1) for url: \(request.url?.absoluteString ?? "")")
124+
print(error)
125+
#endif
126+
127+
var delay: DispatchTimeInterval = .seconds(0)
128+
if retriesLeft <= 3 {
129+
delay = .seconds(retryDelaySeconds)
130+
}
131+
132+
DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
133+
sendRequestWithRetries(request: request, requestId: requestId, retriesLeft: retriesLeft - 1)
134+
}
135+
}
136+
137+
sendRequestWithRetries(request: request, requestId: requestId, retriesLeft: maxRetryCount)
138+
99139
return fulfill
100140
}
101141

tests/unit-tests/IterableAPIResponseTests.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,30 @@ class IterableAPIResponseTests: XCTestCase {
152152
wait(for: [xpectation], timeout: testExpectationTimeout)
153153
}
154154

155+
func testSendRequestWithRetry() {
156+
let xpectation = expectation(description: "retry on status code >= 500")
157+
158+
let networkSession = MockNetworkSession { _ in
159+
MockNetworkSession.MockResponse(statusCode: 503,
160+
data: Data(),
161+
delay: 0)
162+
}
163+
164+
let iterableRequest = IterableRequest.post(PostRequest(path: "", args: nil, body: [:]))
165+
166+
let apiClient = createApiClient(networkSession: networkSession)
167+
var urlRequest = apiClient.convertToURLRequest(iterableRequest: iterableRequest)!
168+
urlRequest.timeoutInterval = 1
169+
170+
RequestSender.sendRequest(urlRequest, usingSession: networkSession).onError { sendError in
171+
xpectation.fulfill()
172+
XCTAssert(sendError.reason!.lowercased().contains("internal server error"))
173+
}
174+
175+
wait(for: [xpectation], timeout: testExpectationTimeout)
176+
}
177+
178+
155179
func testNetworkTimeoutResponse() {
156180
let xpectation = expectation(description: "timeout network response")
157181

0 commit comments

Comments
 (0)