Skip to content

Commit ba21450

Browse files
feat: Retry requests for network errors in Swift (box/box-codegen#820) (#1051)
1 parent b8bf1ad commit ba21450

15 files changed

+315
-280
lines changed

.codegen.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{ "engineHash": "fa469c0", "specHash": "a05e5d7", "version": "0.1.0" }
1+
{ "engineHash": "a356057", "specHash": "a05e5d7", "version": "0.1.0" }

BoxSdkGen/BoxSdkGen.xcodeproj/project.pbxproj

Lines changed: 32 additions & 24 deletions
Large diffs are not rendered by default.

BoxSdkGen/Sources/Box/Errors/BoxAPIError.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ extension BoxAPIError {
6363
json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
6464
}
6565
let responseInfo = ResponseInfo(
66-
statusCode: conversation.urlResponse.statusCode,
67-
headers: conversation.urlResponse.allHeaderFields as? [String: String] ?? [:],
66+
statusCode: conversation.urlResponse?.statusCode ?? 0,
67+
headers: conversation.urlResponse?.allHeaderFields as? [String: String] ?? [:],
6868
body: body,
6969
rawBody: try? Utils.Strings.from(data: body?.toJson() ?? Data()),
7070
code: json?["code"] as? String,

BoxSdkGen/Sources/Internal/Utils.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,4 +436,14 @@ public enum Utils {
436436

437437
return nil
438438
}
439+
440+
/// Generates a random Double value within the specified range.
441+
///
442+
/// - Parameters:
443+
/// - min: The minimum value of the range (inclusive).
444+
/// - max: The maximum value of the range (inclusive).
445+
/// - Returns: A random Double value between min and max.
446+
public static func random(min: Double, max: Double)-> Double {
447+
return Double.random(in: min...max)
448+
}
439449
}

BoxSdkGen/Sources/Networking/BoxNetworkClient.swift

Lines changed: 58 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -35,44 +35,67 @@ public class BoxNetworkClient: NetworkClient {
3535
let memoryInputStream = MemoryInputStream(data: Utils.readByteStream(byteStream: fileStream))
3636
options = options.withFileStream(fileStream: memoryInputStream)
3737
}
38+
let networkSession = options.networkSession ?? NetworkSession()
39+
var currentAttempt = 1
40+
var exceptionRetryCount = 0
41+
var conversation: FetchConversation! = nil
42+
var sdkError: BoxSDKError? = nil
43+
44+
while (true) {
45+
var retryAttemptNumber = currentAttempt
46+
let urlRequest = try await createRequest(
47+
options: options,
48+
networkSession: networkSession
49+
)
50+
51+
if let fileStream = options.fileStream, let memoryInputStream = fileStream as? MemoryInputStream, currentAttempt > 1 {
52+
memoryInputStream.reset()
53+
}
3854

39-
return try await fetch(
40-
options: options,
41-
networkSession: options.networkSession ?? NetworkSession(),
42-
attempt: 1
43-
)
44-
}
55+
do {
56+
if let downloadDestinationUrl = options.downloadDestinationUrl, options.responseFormat == .binary {
57+
let (downloadUrl, urlResponse) = try await sendDownloadRequest(urlRequest, downloadDestinationURL: downloadDestinationUrl)
58+
conversation = FetchConversation(options: options, urlRequest: urlRequest, urlResponse: urlResponse as? HTTPURLResponse, responseType: .url(downloadUrl))
59+
} else {
60+
let (data, urlResponse) = try await sendDataRequest(urlRequest, followRedirects: options.followRedirects ?? true)
61+
conversation = FetchConversation(options: options, urlRequest: urlRequest, urlResponse: urlResponse as? HTTPURLResponse, responseType: .data(data))
62+
}
63+
} catch {
64+
sdkError = (error as? BoxSDKError) ?? BoxSDKError(message: error.localizedDescription, error: error)
65+
conversation = FetchConversation(options: options, urlRequest: urlRequest, urlResponse: nil, responseType: .data(Data()))
66+
exceptionRetryCount += 1
67+
retryAttemptNumber = exceptionRetryCount
68+
}
4569

46-
/// Executes requests
47-
///
48-
/// - Parameters:
49-
/// - options: Request options that provides request-specific information, such as the request type, and body, query parameters.
50-
/// - networkSession: The Networking Session object which provides the URLSession object along with a network configuration parameters used in network communication.
51-
/// - attempt: The request attempt number.
52-
/// - Returns: Response of the request in the form of FetchResponse object.
53-
/// - Throws: An error if the request fails for any reason.
54-
private func fetch(
55-
options: FetchOptions,
56-
networkSession: NetworkSession,
57-
attempt: Int
58-
) async throws -> FetchResponse {
59-
let urlRequest = try await createRequest(
60-
options: options,
61-
networkSession: networkSession
62-
)
63-
64-
if let fileStream = options.fileStream, let memoryInputStream = fileStream as? MemoryInputStream, attempt > 1 {
65-
memoryInputStream.reset()
66-
}
70+
let response = conversation.convertToFetchResponse()
71+
let shouldRetry = try await networkSession.retryStrategy.shouldRetry(
72+
fetchOptions: options,
73+
fetchResponse: response,
74+
attemptNumber: retryAttemptNumber
75+
)
76+
77+
if(shouldRetry){
78+
let retryDelay = networkSession.retryStrategy.retryAfter(
79+
fetchOptions: options,
80+
fetchResponse: response,
81+
attemptNumber: retryAttemptNumber
82+
)
83+
84+
currentAttempt += 1
85+
try await wait(seconds: retryDelay)
86+
continue
87+
}
88+
89+
let statusCode = response.status
90+
if statusCode >= 200 && statusCode < 400 {
91+
return conversation.convertToFetchResponse()
92+
}
6793

68-
if let downloadDestinationUrl = options.downloadDestinationUrl, options.responseFormat == .binary {
69-
let (downloadUrl, urlResponse) = try await sendDownloadRequest(urlRequest, downloadDestinationURL: downloadDestinationUrl)
70-
let conversation = FetchConversation(options: options, urlRequest: urlRequest, urlResponse: urlResponse as! HTTPURLResponse, responseType: .url(downloadUrl))
71-
return try await processResponse(using: conversation, networkSession: networkSession, attempt: attempt)
72-
} else {
73-
let (data, urlResponse) = try await sendDataRequest(urlRequest, followRedirects: options.followRedirects ?? true)
74-
let conversation = FetchConversation(options: options, urlRequest: urlRequest, urlResponse: urlResponse as! HTTPURLResponse, responseType: .data(data))
75-
return try await processResponse(using: conversation, networkSession: networkSession, attempt: attempt)
94+
if let sdkError, statusCode == 0 {
95+
throw sdkError
96+
} else {
97+
throw BoxAPIError(fromConversation: conversation)
98+
}
7699
}
77100
}
78101

@@ -331,50 +354,6 @@ public class BoxNetworkClient: NetworkClient {
331354
return components.url!
332355
}
333356

334-
/// Processes response and performs the appropriate action
335-
///
336-
/// - Parameters:
337-
/// - using: Represents a data combined with the request and the corresponding response.
338-
/// - networkSession: The Networking Session object which provides the URLSession object along with a network configuration parameters used in network communication.
339-
/// - attempt: The request attempt number.
340-
/// - Returns: Response of the request in the form of FetchResponse object.
341-
/// - Throws: An error if the operation fails for any reason.
342-
private func processResponse(
343-
using conversation: FetchConversation,
344-
networkSession: NetworkSession,
345-
attempt: Int
346-
) async throws -> FetchResponse {
347-
let statusCode = conversation.urlResponse.statusCode
348-
let isStatusCodeAcceptedWithRetryAfterHeader = statusCode == 202 && conversation.urlResponse.value(forHTTPHeaderField: HTTPHeaderKey.retryAfter) != nil
349-
350-
// OK
351-
if statusCode >= 200 && statusCode < 400 && (!isStatusCodeAcceptedWithRetryAfterHeader || attempt >= networkSession.networkSettings.maxRetryAttempts) {
352-
return conversation.convertToFetchResponse()
353-
}
354-
355-
// available attempts exceeded
356-
if attempt >= networkSession.networkSettings.maxRetryAttempts {
357-
throw BoxAPIError(fromConversation: conversation, message: "Request has hit the maximum number of retries.")
358-
}
359-
360-
// Unauthorized
361-
if statusCode == 401, let auth = conversation.options.auth {
362-
_ = try await auth.refreshToken(networkSession: networkSession)
363-
return try await fetch(options: conversation.options, networkSession: networkSession, attempt: attempt + 1)
364-
}
365-
366-
// Retryable
367-
if statusCode == 429 || statusCode >= 500 || isStatusCodeAcceptedWithRetryAfterHeader {
368-
let retryTimeout = Double(conversation.urlResponse.value(forHTTPHeaderField: HTTPHeaderKey.retryAfter) ?? "")
369-
?? networkSession.networkSettings.retryStrategy.getRetryTimeout(attempt: attempt)
370-
try await wait(seconds: retryTimeout)
371-
372-
return try await fetch(options: conversation.options, networkSession: networkSession, attempt: attempt + 1)
373-
}
374-
375-
throw BoxAPIError(fromConversation: conversation)
376-
}
377-
378357
/// Suspends the current task for the given duration of seconds.
379358
///
380359
/// - Parameters:

BoxSdkGen/Sources/Networking/DefaultNetworkClient.swift

Lines changed: 58 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -45,44 +45,67 @@ public class DefaultNetworkClient: NetworkClient {
4545
let memoryInputStream = MemoryInputStream(data: Utils.readByteStream(byteStream: fileStream))
4646
options = options.withFileStream(fileStream: memoryInputStream)
4747
}
48+
let networkSession = options.networkSession ?? NetworkSession()
49+
var currentAttempt = 1
50+
var exceptionRetryCount = 0
51+
var conversation: FetchConversation! = nil
52+
var sdkError: BoxSDKError? = nil
53+
54+
while (true) {
55+
var retryAttemptNumber = currentAttempt
56+
let urlRequest = try await createRequest(
57+
options: options,
58+
networkSession: networkSession
59+
)
60+
61+
if let fileStream = options.fileStream, let memoryInputStream = fileStream as? MemoryInputStream, currentAttempt > 1 {
62+
memoryInputStream.reset()
63+
}
4864

49-
return try await fetch(
50-
options: options,
51-
networkSession: options.networkSession ?? NetworkSession(),
52-
attempt: 1
53-
)
54-
}
65+
do {
66+
if let downloadDestinationUrl = options.downloadDestinationUrl, options.responseFormat == .binary {
67+
let (downloadUrl, urlResponse) = try await sendDownloadRequest(urlRequest, downloadDestinationURL: downloadDestinationUrl)
68+
conversation = FetchConversation(options: options, urlRequest: urlRequest, urlResponse: urlResponse as? HTTPURLResponse, responseType: .url(downloadUrl))
69+
} else {
70+
let (data, urlResponse) = try await sendDataRequest(urlRequest, followRedirects: options.followRedirects ?? true)
71+
conversation = FetchConversation(options: options, urlRequest: urlRequest, urlResponse: urlResponse as? HTTPURLResponse, responseType: .data(data))
72+
}
73+
} catch {
74+
sdkError = (error as? BoxSDKError) ?? BoxSDKError(message: error.localizedDescription, error: error)
75+
conversation = FetchConversation(options: options, urlRequest: urlRequest, urlResponse: nil, responseType: .data(Data()))
76+
exceptionRetryCount += 1
77+
retryAttemptNumber = exceptionRetryCount
78+
}
5579

56-
/// Executes requests
57-
///
58-
/// - Parameters:
59-
/// - options: Request options that provides request-specific information, such as the request type, and body, query parameters.
60-
/// - networkSession: The Networking Session object which provides the URLSession object along with a network configuration parameters used in network communication.
61-
/// - attempt: The request attempt number.
62-
/// - Returns: Response of the request in the form of FetchResponse object.
63-
/// - Throws: An error if the request fails for any reason.
64-
private func fetch(
65-
options: FetchOptions,
66-
networkSession: NetworkSession,
67-
attempt: Int
68-
) async throws -> FetchResponse {
69-
let urlRequest = try await createRequest(
70-
options: options,
71-
networkSession: networkSession
72-
)
73-
74-
if let fileStream = options.fileStream, let memoryInputStream = fileStream as? MemoryInputStream, attempt > 1 {
75-
memoryInputStream.reset()
76-
}
80+
let response = conversation.convertToFetchResponse()
81+
let shouldRetry = try await networkSession.retryStrategy.shouldRetry(
82+
fetchOptions: options,
83+
fetchResponse: response,
84+
attemptNumber: retryAttemptNumber
85+
)
86+
87+
if(shouldRetry){
88+
let retryDelay = networkSession.retryStrategy.retryAfter(
89+
fetchOptions: options,
90+
fetchResponse: response,
91+
attemptNumber: retryAttemptNumber
92+
)
93+
94+
currentAttempt += 1
95+
try await wait(seconds: retryDelay)
96+
continue
97+
}
98+
99+
let statusCode = response.status
100+
if statusCode >= 200 && statusCode < 400 {
101+
return conversation.convertToFetchResponse()
102+
}
77103

78-
if let downloadDestinationUrl = options.downloadDestinationUrl, options.responseFormat == .binary {
79-
let (downloadUrl, urlResponse) = try await sendDownloadRequest(urlRequest, downloadDestinationURL: downloadDestinationUrl)
80-
let conversation = FetchConversation(options: options, urlRequest: urlRequest, urlResponse: urlResponse as! HTTPURLResponse, responseType: .url(downloadUrl))
81-
return try await processResponse(using: conversation, networkSession: networkSession, attempt: attempt)
82-
} else {
83-
let (data, urlResponse) = try await sendDataRequest(urlRequest, followRedirects: options.followRedirects ?? true)
84-
let conversation = FetchConversation(options: options, urlRequest: urlRequest, urlResponse: urlResponse as! HTTPURLResponse, responseType: .data(data))
85-
return try await processResponse(using: conversation, networkSession: networkSession, attempt: attempt)
104+
if let sdkError, statusCode == 0 {
105+
throw sdkError
106+
} else {
107+
throw BoxAPIError(fromConversation: conversation)
108+
}
86109
}
87110
}
88111

@@ -336,49 +359,6 @@ public class DefaultNetworkClient: NetworkClient {
336359
return components.url!
337360
}
338361

339-
/// Processes response and performs the appropriate action
340-
///
341-
/// - Parameters:
342-
/// - using: Represents a data combined with the request and the corresponding response.
343-
/// - networkSession: The Networking Session object which provides the URLSession object along with a network configuration parameters used in network communication.
344-
/// - attempt: The request attempt number.
345-
/// - Returns: Response of the request in the form of FetchResponse object.
346-
/// - Throws: An error if the operation fails for any reason.
347-
private func processResponse(
348-
using conversation: FetchConversation,
349-
networkSession: NetworkSession,
350-
attempt: Int
351-
) async throws -> FetchResponse {
352-
let statusCode = conversation.urlResponse.statusCode
353-
let isStatusCodeAcceptedWithRetryAfterHeader = statusCode == 202 && conversation.urlResponse.value(forHTTPHeaderField: HTTPHeaderKey.retryAfter) != nil
354-
355-
// OK
356-
if statusCode >= 200 && statusCode < 400 && (!isStatusCodeAcceptedWithRetryAfterHeader || attempt >= networkSession.networkSettings.maxRetryAttempts) {
357-
return conversation.convertToFetchResponse()
358-
}
359-
360-
// available attempts exceeded
361-
if attempt >= networkSession.networkSettings.maxRetryAttempts {
362-
throw BoxAPIError(fromConversation: conversation, message: "Request has hit the maximum number of retries.")
363-
}
364-
365-
// Unauthorized
366-
if statusCode == 401, let auth = conversation.options.auth {
367-
_ = try await auth.refreshToken(networkSession: networkSession)
368-
return try await fetch(options: conversation.options, networkSession: networkSession, attempt: attempt + 1)
369-
}
370-
371-
// Retryable
372-
if statusCode == 429 || statusCode >= 500 || isStatusCodeAcceptedWithRetryAfterHeader {
373-
let retryTimeout = Double(conversation.urlResponse.value(forHTTPHeaderField: HTTPHeaderKey.retryAfter) ?? "")
374-
?? networkSession.networkSettings.retryStrategy.getRetryTimeout(attempt: attempt)
375-
try await wait(seconds: retryTimeout)
376-
377-
return try await fetch(options: conversation.options, networkSession: networkSession, attempt: attempt + 1)
378-
}
379-
380-
throw BoxAPIError(fromConversation: conversation)
381-
}
382362

383363
/// Suspends the current task for the given duration of seconds.
384364
///

BoxSdkGen/Sources/Networking/FetchConversation.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class FetchConversation {
1616
/// Represents an URL request.
1717
let urlRequest: URLRequest
1818
/// Represents a response to an HTTP URL.
19-
let urlResponse: HTTPURLResponse
19+
let urlResponse: HTTPURLResponse?
2020
/// Represents response type, either data or downloaded file
2121
let responseType: ResponseType
2222

@@ -28,7 +28,7 @@ class FetchConversation {
2828
/// - urlRequest: Represents an URL request.
2929
/// - urlResponse: Represents a response to an HTTP URL
3030
/// - responseType: Represents response type, either data or downloaded file
31-
init(options: FetchOptions, urlRequest: URLRequest, urlResponse: HTTPURLResponse, responseType: ResponseType) {
31+
init(options: FetchOptions, urlRequest: URLRequest, urlResponse: HTTPURLResponse?, responseType: ResponseType) {
3232
self.options = options
3333
self.urlRequest = urlRequest
3434
self.urlResponse = urlResponse
@@ -39,6 +39,10 @@ class FetchConversation {
3939
///
4040
/// - Returns: An instance of the`FetchResponse`.
4141
func convertToFetchResponse() -> FetchResponse {
42+
guard let urlResponse else {
43+
return FetchResponse(status: 0, headers: [:])
44+
}
45+
4246
let headers: [String: String] = urlResponse.allHeaderFields.reduce(into: [:]) { result, pair in
4347
if let key = pair.key as? String, let value = pair.value as? String {
4448
result[key] = value

0 commit comments

Comments
 (0)