@@ -70,19 +70,39 @@ public extension APIClient.Configs {
7070 }
7171
7272 /// The interval between retries. It can be a fixed time interval or a closure that takes the current retry count and returns a time interval.
73+ /// Default to exponential backoff starting at 0.5 seconds, doubling each time, up to a maximum of 30 seconds.
7374 /// - Note: This configuration works only if you use the `retry()` modifier.
74- var retryInterval : ( Int ) -> TimeInterval {
75- get { self [ \. retryInterval] ?? { _ in 1.0 } }
75+ var retryInterval : ( _ attempt: Int , _ response: HTTPResponse ? ) -> TimeInterval {
76+ get {
77+ self [ \. retryInterval] ?? { attempt, response in
78+ min ( 0.5 * pow( 2.0 , Double ( attempt) ) , 30.0 )
79+ }
80+ }
7681 set { self [ \. retryInterval] = newValue }
7782 }
78-
83+
7984 /// The date formatter used to parse the `Retry-After` header when it contains a date. By default, it uses the RFC 1123 format.
8085 /// - Tips: `DateFormatter` creation is expensive, so if you need a custom format, create the formatter once and reuse it.
8186 var retryAfterHeaderDateFormatter : DateFormatter {
8287 get { self [ \. retryAfterHeaderDateFormatter] ?? defaultRetryAfterHeaderDateFormatter }
8388 set { self [ \. retryAfterHeaderDateFormatter] = newValue }
8489 }
8590
91+ /// The set of HTTP status codes that may include a `Retry-After` header. Default to `[429, 503]` due to RFC 7231.
92+ /// If a response has one of these status codes and includes a `Retry-After` header,
93+ /// the client will wait for the specified duration before retrying the request.
94+ /// - Note: This configuration works only if you use the `retry()` modifier.
95+ var retryAfterHeaderStatusCodes : Set < HTTPResponse . Status > {
96+ get { self [ \. retryAfterHeaderStatusCodes] ?? [ . tooManyRequests, . serviceUnavailable] }
97+ set { self [ \. retryAfterHeaderStatusCodes] = newValue }
98+ }
99+
100+ /// Configuration for jitter applied to retry intervals.
101+ var retryJitterConfigs : RetryJitterConfigs {
102+ get { self [ \. retryJitterConfigs] ?? RetryJitterConfigs ( ) }
103+ set { self [ \. retryJitterConfigs] = newValue }
104+ }
105+
86106 /// The backoff policy used to determine how to handle global backoff scenarios, such as rate limiting.
87107 /// - Note: This configuration works only if you use the `retry()` modifier.
88108 var retryBackoffPolicy : RetryBackoffPolicy {
@@ -110,15 +130,15 @@ public extension APIClient {
110130 /// Sets the interval between retries. It can be a fixed time interval or a closure that takes the current retry count and returns a time interval.
111131 /// - Parameter interval: A closure that takes the current retry count (starting from 0
112132 /// - Note: This configuration works only if you use the `retry()` modifier.
113- func retryInterval( _ interval: @escaping ( Int ) -> TimeInterval ) -> APIClient {
133+ func retryInterval( _ interval: @escaping ( Int , HTTPResponse ? ) -> TimeInterval ) -> APIClient {
114134 configs ( \. retryInterval, interval)
115135 }
116136
117137 /// Sets a fixed interval between retries.
118138 /// - Parameter interval: The time interval to wait before the next retry.
119139 /// - Note: This configuration works only if you use the `retry()` modifier.
120140 func retryInterval( _ interval: TimeInterval ) -> APIClient {
121- retryInterval { _ in interval }
141+ retryInterval { _, _ in interval }
122142 }
123143
124144 /// Sets the date formatter used to parse the `Retry-After` header when it contains a date. By default, it uses the RFC 1123 format.
@@ -138,7 +158,8 @@ public extension APIClient {
138158
139159 /// Retries the request when necessary, based on the configured retry conditions and limits.
140160 /// - Note: Like any modifier, this is order dependent. It takes in account only error from previous modifiers but not the following ones.
141- /// - Tip: You can customize the retry behavior by setting the `retryCondition`, `retryLimit`, `retryInterval`, and `retryBackoffPolicy` configurations.
161+ /// - Tip: You can customize the retry behavior by setting the `retryCondition`, `retryLimit`, `retryInterval`, `retryBackoffPolicy`,
162+ /// `retryAfterHeaderStatusCodes`, `retryAfterHeaderDateFormatter` and `retryJitterConfigs` configurations.
142163 func retry( ) -> APIClient {
143164 httpClientMiddleware ( retryMiddleware ( ) )
144165 }
@@ -147,13 +168,13 @@ public extension APIClient {
147168/// A condition that determines whether a request should be retried based on the request, the result of the request, and the client configurations.
148169public struct RetryRequestCondition {
149170
150- private let condition : ( HTTPRequestComponents , Result < HTTPResponse , Error > , APIClient . Configs ) -> Bool
171+ private let condition : ( HTTPRequestComponents , HTTPResponse ? , Error ? , APIClient . Configs ) -> Bool
151172
152173 /// Initializes a new `RetryRequestCondition` with a custom condition closure.
153174 /// - Parameters:
154175 /// - condition: A closure that takes the request, the result of the request, and the client configs, and returns a Boolean indicating whether to retry the request.
155176 public init (
156- _ condition: @escaping ( _ request: HTTPRequestComponents , _ result: Result < HTTPResponse , Error > , _ configs: APIClient . Configs ) -> Bool
177+ _ condition: @escaping ( _ request: HTTPRequestComponents , _ result: HTTPResponse ? , _ error : Error ? , _ configs: APIClient . Configs ) -> Bool
157178 ) {
158179 self . condition = condition
159180 }
@@ -165,19 +186,20 @@ public struct RetryRequestCondition {
165186 /// - configs: The configurations of the API client.
166187 public func shouldRetry(
167188 request: HTTPRequestComponents ,
168- result: Result < HTTPResponse , Error > ,
189+ response: HTTPResponse ? ,
190+ error: Error ? ,
169191 configs: APIClient . Configs
170192 ) -> Bool {
171- condition ( request, result , configs)
193+ condition ( request, response , error , configs)
172194 }
173195
174196 /// Combines two `RetryRequestCondition` instances using a logical AND operation.
175197 /// The resulting condition will only return `true` if both conditions return `true`.
176198 /// - Parameter other: Another `RetryRequestCondition` to combine with.
177199 public func and( _ other: RetryRequestCondition ) -> RetryRequestCondition {
178- RetryRequestCondition { request, result , configs in
179- self . shouldRetry ( request: request, result : result , configs: configs)
180- && other. shouldRetry ( request: request, result : result , configs: configs)
200+ RetryRequestCondition { request, response , error , configs in
201+ self . shouldRetry ( request: request, response : response , error : error , configs: configs)
202+ && other. shouldRetry ( request: request, response : response , error : error , configs: configs)
181203 }
182204 }
183205
@@ -194,9 +216,9 @@ public struct RetryRequestCondition {
194216 /// - Parameter conditions: An array of `RetryRequestCondition` instances to combine.
195217 /// - Returns: A new `RetryRequestCondition` that represents the combined conditions.
196218 public static func and( _ conditions: [ RetryRequestCondition ] ) -> RetryRequestCondition {
197- RetryRequestCondition { request, result , configs in
219+ RetryRequestCondition { request, response , error , configs in
198220 for condition in conditions {
199- if !condition. shouldRetry ( request: request, result : result , configs: configs) {
221+ if !condition. shouldRetry ( request: request, response : response , error : error , configs: configs) {
200222 return false
201223 }
202224 }
@@ -217,9 +239,9 @@ public struct RetryRequestCondition {
217239 /// - Parameter conditions: An array of `RetryRequestCondition` instances to combine.
218240 /// - Returns: A new `RetryRequestCondition` that represents the combined conditions.
219241 public static func or( _ conditions: [ RetryRequestCondition ] ) -> RetryRequestCondition {
220- RetryRequestCondition { request, result , configs in
242+ RetryRequestCondition { request, response , error , configs in
221243 for condition in conditions {
222- if condition. shouldRetry ( request: request, result : result , configs: configs) {
244+ if condition. shouldRetry ( request: request, response : response , error : error , configs: configs) {
223245 return true
224246 }
225247 }
@@ -231,9 +253,9 @@ public struct RetryRequestCondition {
231253 /// The resulting condition will return `true` if either condition returns `true`.
232254 /// - Parameter other: Another `RetryRequestCondition` to combine with.
233255 public func or( _ other: RetryRequestCondition ) -> RetryRequestCondition {
234- RetryRequestCondition { request, result , configs in
235- self . shouldRetry ( request: request, result : result , configs: configs)
236- || other. shouldRetry ( request: request, result : result , configs: configs)
256+ RetryRequestCondition { request, response , error , configs in
257+ self . shouldRetry ( request: request, response : response , error : error , configs: configs)
258+ || other. shouldRetry ( request: request, response : response , error : error , configs: configs)
237259 }
238260 }
239261
@@ -254,17 +276,17 @@ public struct RetryRequestCondition {
254276 }
255277
256278 /// A `RetryRequestCondition` that retries safe HTTP methods (like GET) when the request fails due to error status codes or network errors.
257- public static let requestFailed = RetryRequestCondition { request, result , _ in
258- switch result {
259- case let . success ( response ) :
279+ public static let requestFailed = RetryRequestCondition { request, response , error , _ in
280+ switch error {
281+ case nil :
260282 return false
261- case let . failure ( error) :
283+ case let . some ( error) :
262284 return !( error is CancellationError )
263285 }
264286 }
265287
266288 /// A `RetryRequestCondition` that retries the request when the HTTP method is considered safe (e.g., GET, HEAD, OPTIONS).
267- public static let requestMethodIsSafe = RetryRequestCondition { request, _, _ in
289+ public static let requestMethodIsSafe = RetryRequestCondition { request, _, _, _ in
268290 request. method. isSafe
269291 }
270292
@@ -281,7 +303,7 @@ public struct RetryRequestCondition {
281303
282304 /// A `RetryRequestCondition` that retries requests with defined HTTP methods.
283305 public static func methods( _ methods: Set < HTTPRequest . Method > ) -> RetryRequestCondition {
284- RetryRequestCondition { request, _, _ in
306+ RetryRequestCondition { request, _, _, _ in
285307 methods. contains ( request. method)
286308 }
287309 }
@@ -293,8 +315,8 @@ public struct RetryRequestCondition {
293315
294316 /// A `RetryRequestCondition` that retries when the response status code is one of the specified codes.
295317 public static func statusCodes( _ codes: Set < HTTPResponse . Status > ) -> RetryRequestCondition {
296- RetryRequestCondition { _, response, _ in
297- if case let . success ( response ) = response {
318+ RetryRequestCondition { _, response, _, _ in
319+ if let response {
298320 return codes. contains ( response. status)
299321 }
300322 return false
@@ -340,15 +362,15 @@ private struct retryMiddleware: HTTPClientMiddleware {
340362 let backoffPolicy = configs. retryBackoffPolicy
341363 if let hash = backoffPolicy. scopeHash ( request) {
342364 if let interval = await waitForSynchronizedAccess ( id: hash, of: UInt64 . self) {
343- try await Task . sleep ( nanoseconds: jitterNs ( interval) )
365+ try await Task . sleep ( nanoseconds: configs . retryJitterConfigs . delay ( for : interval) )
344366 }
345367 }
346368 var count = 0
347369 var response : HTTPResponse ?
348370 var retryAfterHeader : TimeInterval = 0
349371
350- func needRetry( _ result : Result < HTTPResponse , Error > ) -> Bool {
351- guard condition. shouldRetry ( request: request, result : result , configs: configs) else {
372+ func needRetry( _ error : Error ? ) -> Bool {
373+ guard condition. shouldRetry ( request: request, response : response , error : error , configs: configs) else {
352374 return false
353375 }
354376 if let limit {
@@ -359,7 +381,7 @@ private struct retryMiddleware: HTTPClientMiddleware {
359381
360382 func retry( ) async throws -> ( T , HTTPResponse ) {
361383 if count > 0 {
362- let interval = UInt64 ( max ( retryAfterHeader, interval ( count - 1 ) ) * 1_000_000_000 )
384+ let interval = UInt64 ( max ( retryAfterHeader, interval ( count - 1 , response ) ) * 1_000_000_000 )
363385 if interval > 0 {
364386 if let response, let hash = backoffPolicy. scopeHash ( request) , backoffPolicy. isGlobalBackoff ( request, response. status) {
365387 _ = try await withThrowingSynchronizedAccess ( id: hash) {
@@ -372,8 +394,14 @@ private struct retryMiddleware: HTTPClientMiddleware {
372394 }
373395 }
374396 count += 1
375- let ( result, rsp) = try await extractResponseEvenFailed {
376- try await next ( request, configs)
397+ let ( result, rsp) : ( Result < ( T , HTTPResponse ) , Error > , HTTPResponse )
398+ do {
399+ ( result, rsp) = try await extractResponseEvenFailed {
400+ try await next ( request, configs)
401+ }
402+ } catch {
403+ response = nil
404+ throw error
377405 }
378406 response = rsp
379407 return try result. get ( )
@@ -382,17 +410,18 @@ private struct retryMiddleware: HTTPClientMiddleware {
382410 while true {
383411 do {
384412 let ( data, httpResponse) = try await retry ( )
385- // 429 and 503 may include Retry-After header by RFC 7231
386- if [ 429 , 503 ] . contains ( httpResponse. status. code) {
413+ if configs. retryAfterHeaderStatusCodes. contains ( httpResponse. status) {
387414 retryAfterHeader = httpResponse. headerFields [ . retryAfter] . flatMap {
388415 decodeRetryAfterHeader ( $0, formatter: configs. retryAfterHeaderDateFormatter)
389416 } ?? 0
390417 }
391- if !needRetry( . success ( httpResponse ) ) {
418+ if !needRetry( nil ) {
392419 return ( data, httpResponse)
393420 }
394- } catch {
395- if !needRetry( . failure( error) ) {
421+ } catch is CancellationError {
422+ throw CancellationError ( )
423+ } catch {
424+ if !needRetry( error) {
396425 throw error
397426 }
398427 }
@@ -401,18 +430,36 @@ private struct retryMiddleware: HTTPClientMiddleware {
401430 }
402431}
403432
404- @inline ( __always)
405- private func jitterNs(
406- _ base: UInt64 ,
407- fraction: ClosedRange < Double > = 0.1 ... 0.2 ,
408- minNs: UInt64 = 5_000_000 , // 5 ms
409- maxNs: UInt64 = 1_000_000_000
410- ) // 1 s
411- -> UInt64 {
412- guard base > 0 else { return 0 }
413- let p = Double . random ( in: fraction)
414- let raw = UInt64 ( Double ( base) * p)
415- return min ( max ( raw, minNs) , maxNs)
433+ /// Configuration for jitter applied to retry intervals.
434+ public struct RetryJitterConfigs : Hashable {
435+
436+ /// The fraction range of the base interval to use for jitter.
437+ public var fraction : ClosedRange < Double >
438+
439+ /// The minimum jitter in nanoseconds.
440+ public var minNs : UInt64
441+
442+ /// The maximum jitter in nanoseconds.
443+ public var maxNs : UInt64
444+
445+ public init (
446+ fraction: ClosedRange < Double > = 0.1 ... 0.2 ,
447+ minNs: UInt64 = 5_000_000 , // 5 ms
448+ maxNs: UInt64 = 1_000_000_000 // 1 s
449+ ) {
450+ self . fraction = fraction
451+ self . minNs = minNs
452+ self . maxNs = maxNs
453+ }
454+
455+ /// Applies jitter to the given interval.
456+ @inline ( __always)
457+ public func delay( for interval: UInt64 ) -> UInt64 {
458+ guard interval > 0 else { return 0 }
459+ let p = Double . random ( in: fraction)
460+ let raw = UInt64 ( Double ( interval) * p)
461+ return min ( max ( raw, minNs) , maxNs)
462+ }
416463}
417464
418465private func decodeRetryAfterHeader( _ value: String , formatter: DateFormatter ) -> TimeInterval ? {
0 commit comments