@@ -7,11 +7,21 @@ struct CloudflareClient: Sendable {
77 let credentials : CloudflareCredentials
88 let session : URLSession
99 let timeout : Duration
10-
11- init ( credentials: CloudflareCredentials , session: URLSession = . shared, timeout: Duration = . seconds( 60 ) ) {
10+ let maxRetryCount : Int
11+ let retryBaseDelay : Duration
12+
13+ init (
14+ credentials: CloudflareCredentials ,
15+ session: URLSession = . shared,
16+ timeout: Duration = . seconds( 60 ) ,
17+ maxRetryCount: Int = 2 ,
18+ retryBaseDelay: Duration = . milliseconds( 300 )
19+ ) {
1220 self . credentials = credentials
1321 self . session = session
1422 self . timeout = timeout
23+ self . maxRetryCount = max ( 0 , maxRetryCount)
24+ self . retryBaseDelay = retryBaseDelay
1525 }
1626
1727 func toMarkdown( files: [ ( data: Data , filename: String ) ] ) async throws -> [ ConversionResult ] {
@@ -33,39 +43,93 @@ struct CloudflareClient: Sendable {
3343 request. setValue ( multipart. contentType, forHTTPHeaderField: " Content-Type " )
3444 request. httpBody = multipart. finalize ( )
3545
36- let data : Data
37- let response : URLResponse
38-
39- do {
40- ( data, response) = try await session. data ( for: request)
41- } catch {
42- throw Swift2MDError . networkError ( underlying: error)
46+ var attempt = 0
47+
48+ while true {
49+ do {
50+ let ( data, response) = try await session. data ( for: request)
51+
52+ guard let httpResponse = response as? HTTPURLResponse else {
53+ throw Swift2MDError . invalidResponse
54+ }
55+
56+ let responseBody = String ( data: data, encoding: . utf8) ?? " "
57+ guard ( 200 ... 299 ) . contains ( httpResponse. statusCode) else {
58+ if shouldRetry ( statusCode: httpResponse. statusCode, attempt: attempt) {
59+ throw RetryableRequestError ( )
60+ }
61+ throw Swift2MDError . httpError ( statusCode: httpResponse. statusCode, body: responseBody)
62+ }
63+
64+ do {
65+ let decoded = try JSONDecoder ( ) . decode ( APIEnvelope . self, from: data)
66+ guard decoded. success else {
67+ let messages = decoded. errors. map ( \. message) + decoded. messages. map ( \. message)
68+ throw Swift2MDError . apiError ( messages: messages)
69+ }
70+ return decoded. result
71+ } catch let error as Swift2MDError {
72+ throw error
73+ } catch {
74+ throw Swift2MDError . invalidResponse
75+ }
76+ } catch is RetryableRequestError {
77+ try await sleepBeforeRetry ( attempt: attempt)
78+ attempt += 1
79+ continue
80+ } catch let error as Swift2MDError {
81+ throw error
82+ } catch {
83+ if shouldRetry ( networkError: error, attempt: attempt) {
84+ try await sleepBeforeRetry ( attempt: attempt)
85+ attempt += 1
86+ continue
87+ }
88+ throw Swift2MDError . networkError ( underlying: error)
89+ }
4390 }
91+ }
4492
45- guard let httpResponse = response as? HTTPURLResponse else {
46- throw Swift2MDError . invalidResponse
47- }
93+ private func shouldRetry( statusCode: Int , attempt: Int ) -> Bool {
94+ guard attempt < maxRetryCount else { return false }
95+ return statusCode == 429 || statusCode >= 500
96+ }
4897
49- let responseBody = String ( data: data, encoding: . utf8) ?? " "
50- guard ( 200 ... 299 ) . contains ( httpResponse. statusCode) else {
51- throw Swift2MDError . httpError ( statusCode: httpResponse. statusCode, body: responseBody)
98+ private func shouldRetry( networkError: Error , attempt: Int ) -> Bool {
99+ guard attempt < maxRetryCount else { return false }
100+ if networkError is CancellationError { return false }
101+
102+ guard let urlError = networkError as? URLError else { return false }
103+ switch urlError. code {
104+ case . timedOut,
105+ . cannotFindHost,
106+ . cannotConnectToHost,
107+ . networkConnectionLost,
108+ . dnsLookupFailed,
109+ . notConnectedToInternet,
110+ . internationalRoamingOff,
111+ . callIsActive,
112+ . dataNotAllowed,
113+ . resourceUnavailable:
114+ return true
115+ default :
116+ return false
52117 }
118+ }
53119
54- do {
55- let decoded = try JSONDecoder ( ) . decode ( APIEnvelope . self, from: data)
56- guard decoded. success else {
57- let messages = decoded. errors. map ( \. message) + decoded. messages. map ( \. message)
58- throw Swift2MDError . apiError ( messages: messages)
59- }
60- return decoded. result
61- } catch let error as Swift2MDError {
62- throw error
63- } catch {
64- throw Swift2MDError . invalidResponse
65- }
120+ private func sleepBeforeRetry( attempt: Int ) async throws {
121+ let baseSeconds = max ( 0 , retryBaseDelay. timeInterval)
122+ guard baseSeconds > 0 else { return }
123+
124+ let delaySeconds = baseSeconds * pow( 2.0 , Double ( attempt) )
125+ let delayNanoseconds = UInt64 ( min ( delaySeconds * 1_000_000_000 , Double ( UInt64 . max) ) )
126+ try await Task . sleep ( nanoseconds: delayNanoseconds)
66127 }
67128}
68129
130+ private struct RetryableRequestError : Error {
131+ }
132+
69133private struct APIEnvelope : Decodable {
70134 let result : [ ConversionResult ]
71135 let success : Bool
0 commit comments