• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / supabase-swift / 29841381594

21 Jul 2026 02:54PM UTC coverage: 83.593% (-0.1%) from 83.725%
29841381594

Pull #1128

github

web-flow
Merge a9b6ff837 into 646cb9f81
Pull Request #1128: ci: weekly SDK compliance sync via supabase/sdk reusable workflow

8142 of 9740 relevant lines covered (83.59%)

1102442.47 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.43
/Sources/Helpers/HTTP/RetryRequestInterceptor.swift
1
//
2
//  RetryRequestInterceptor.swift
3
//
4
//
5
//  Created by Guilherme Souza on 23/04/24.
6
//
7

8
package import Foundation
9
package import HTTPTypes
10

11
#if canImport(FoundationNetworking)
12
  import FoundationNetworking
13
#endif
14

15
/// An HTTP client interceptor for retrying failed HTTP requests with exponential backoff.
16
///
17
/// The `RetryRequestInterceptor` actor intercepts HTTP requests and automatically retries them in case
18
/// of failure, with exponential backoff between retries. You can configure the retry behavior by specifying
19
/// the retry limit, exponential backoff base, scale, retryable HTTP methods, HTTP status codes, and URL error codes.
20
package actor RetryRequestInterceptor: HTTPClientInterceptor {
21
  /// The default retry limit for the interceptor.
22
  package static let defaultRetryLimit = 2
23
  /// The default base value for exponential backoff.
24
  package static let defaultExponentialBackoffBase: UInt = 2
25
  /// The default scale factor for exponential backoff.
26
  package static let defaultExponentialBackoffScale: Double = 0.5
27

28
  /// The default set of retryable HTTP methods.
29
  package static let defaultRetryableHTTPMethods: Set<HTTPTypes.HTTPRequest.Method> = [
30
    .delete, .get, .head, .options, .put, .trace,
31
  ]
32

33
  /// The default set of retryable URL error codes.
34
  package static let defaultRetryableURLErrorCodes: Set<URLError.Code> = [
35
    .backgroundSessionInUseByAnotherProcess, .backgroundSessionWasDisconnected,
36
    .badServerResponse, .callIsActive, .cannotConnectToHost, .cannotFindHost,
37
    .cannotLoadFromNetwork, .dataNotAllowed, .dnsLookupFailed,
38
    .downloadDecodingFailedMidStream, .downloadDecodingFailedToComplete,
39
    .internationalRoamingOff, .networkConnectionLost, .notConnectedToInternet,
40
    .secureConnectionFailed, .serverCertificateHasBadDate,
41
    .serverCertificateNotYetValid, .timedOut,
42
  ]
43

44
  /// The default set of retryable HTTP status codes.
45
  ///
46
  /// Includes Cloudflare-specific error codes (520-524, 530) which represent transient
47
  /// infrastructure errors that should not cause session invalidation.
48
  package static let defaultRetryableHTTPStatusCodes: Set<Int> = [
49
    408, 500, 502, 503, 504,
50
    // Cloudflare-specific transient errors
51
    520, 521, 522, 523, 524, 530,
52
  ]
53

54
  /// The maximum number of retries.
55
  package let retryLimit: Int
56
  /// The base value for exponential backoff.
57
  package let exponentialBackoffBase: UInt
58
  /// The scale factor for exponential backoff.
59
  package let exponentialBackoffScale: Double
60
  /// The set of retryable HTTP methods.
61
  package let retryableHTTPMethods: Set<HTTPTypes.HTTPRequest.Method>
62
  /// The set of retryable HTTP status codes.
63
  package let retryableHTTPStatusCodes: Set<Int>
64
  /// The set of retryable URL error codes.
65
  package let retryableErrorCodes: Set<URLError.Code>
66
  /// The clock used to wait between retries.
67
  package let clock: any Clock<Duration>
68

69
  /// Creates a `RetryRequestInterceptor` instance.
70
  ///
71
  /// - Parameters:
72
  ///   - retryLimit: The maximum number of retries. Default is `2`.
73
  ///   - exponentialBackoffBase: The base value for exponential backoff. Default is `2`.
74
  ///   - exponentialBackoffScale: The scale factor for exponential backoff. Default is `0.5`.
75
  ///   - retryableHTTPMethods: The set of retryable HTTP methods. Default includes common methods.
76
  ///   - retryableHTTPStatusCodes: The set of retryable HTTP status codes. Default includes common status codes.
77
  ///   - retryableErrorCodes: The set of retryable URL error codes. Default includes common error codes.
78
  ///   - clock: The clock used to wait between retries. Default is `ContinuousClock()`.
79
  package init(
80
    retryLimit: Int = RetryRequestInterceptor.defaultRetryLimit,
81
    exponentialBackoffBase: UInt = RetryRequestInterceptor.defaultExponentialBackoffBase,
82
    exponentialBackoffScale: Double = RetryRequestInterceptor.defaultExponentialBackoffScale,
83
    retryableHTTPMethods: Set<HTTPTypes.HTTPRequest.Method> = RetryRequestInterceptor
84
      .defaultRetryableHTTPMethods,
85
    retryableHTTPStatusCodes: Set<Int> = RetryRequestInterceptor.defaultRetryableHTTPStatusCodes,
86
    retryableErrorCodes: Set<URLError.Code> = RetryRequestInterceptor.defaultRetryableURLErrorCodes,
87
    clock: any Clock<Duration> = ContinuousClock()
88
  ) {
170✔
89
    precondition(
170✔
90
      exponentialBackoffBase >= 2,
170✔
91
      "The `exponentialBackoffBase` must be a minimum of 2."
170✔
92
    )
170✔
93

170✔
94
    self.retryLimit = retryLimit
170✔
95
    self.exponentialBackoffBase = exponentialBackoffBase
170✔
96
    self.exponentialBackoffScale = exponentialBackoffScale
170✔
97
    self.retryableHTTPMethods = retryableHTTPMethods
170✔
98
    self.retryableHTTPStatusCodes = retryableHTTPStatusCodes
170✔
99
    self.retryableErrorCodes = retryableErrorCodes
170✔
100
    self.clock = clock
170✔
101
  }
170✔
102

103
  /// Intercepts an HTTP request and automatically retries it in case of failure.
104
  ///
105
  /// - Parameters:
106
  ///   - request: The original HTTP request to be intercepted and retried.
107
  ///   - next: A closure representing the next interceptor in the chain.
108
  /// - Returns: The HTTP response obtained after retrying.
109
  package func intercept(
110
    _ request: HTTPRequest,
111
    next: @Sendable (HTTPRequest) async throws -> HTTPResponse
112
  ) async throws -> HTTPResponse {
157✔
113
    try await retry(request, retryCount: 1, next: next)
157✔
114
  }
113✔
115

116
  private func shouldRetry(request: HTTPRequest, result: Result<HTTPResponse, any Error>) -> Bool {
157✔
117
    guard retryableHTTPMethods.contains(request.method) else { return false }
157✔
118

155✔
119
    if let statusCode = result.value?.statusCode, retryableHTTPStatusCodes.contains(statusCode) {
155✔
120
      return true
13✔
121
    }
142✔
122

142✔
123
    guard let errorCode = (result.error as? URLError)?.code else {
142✔
124
      return false
142✔
125
    }
142✔
126

×
127
    return retryableErrorCodes.contains(errorCode)
×
128
  }
157✔
129

130
  private func retry(
131
    _ request: HTTPRequest,
132
    retryCount: Int,
133
    next: @Sendable (HTTPRequest) async throws -> HTTPResponse
134
  ) async throws -> HTTPResponse {
170✔
135
    let result: Result<HTTPResponse, any Error>
170✔
136

170✔
137
    do {
170✔
138
      let response = try await next(request)
170✔
139
      result = .success(response)
126✔
140
    } catch {
126✔
141
      result = .failure(error)
44✔
142
    }
170✔
143

170✔
144
    if retryCount < retryLimit, shouldRetry(request: request, result: result) {
170✔
145
      let retryDelay =
13✔
146
        pow(
13✔
147
          Double(exponentialBackoffBase),
13✔
148
          Double(retryCount)
13✔
149
        ) * exponentialBackoffScale
13✔
150

13✔
151
      try? await clock.sleep(for: .seconds(retryDelay))
13✔
152

13✔
153
      if !Task.isCancelled {
13✔
154
        return try await retry(request, retryCount: retryCount + 1, next: next)
13✔
155
      }
13✔
156
    }
157✔
157

157✔
158
    return try result.get()
157✔
159
  }
170✔
160
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc