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

supabase / supabase-swift / 28984874484

09 Jul 2026 12:17AM UTC coverage: 83.136% (-0.09%) from 83.221%
28984874484

Pull #1098

github

web-flow
Merge edacea08c into 27f31bfb7
Pull Request #1098: test(auth): migrate AuthTests to Swift Testing + Replay (Phase 5)

7873 of 9470 relevant lines covered (83.14%)

1360641.08 hits per line

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

94.64
/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

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

156✔
90
    self.retryLimit = retryLimit
156✔
91
    self.exponentialBackoffBase = exponentialBackoffBase
156✔
92
    self.exponentialBackoffScale = exponentialBackoffScale
156✔
93
    self.retryableHTTPMethods = retryableHTTPMethods
156✔
94
    self.retryableHTTPStatusCodes = retryableHTTPStatusCodes
156✔
95
    self.retryableErrorCodes = retryableErrorCodes
156✔
96
  }
156✔
97

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

111
  private func shouldRetry(request: HTTPRequest, result: Result<HTTPResponse, any Error>) -> Bool {
149✔
112
    guard retryableHTTPMethods.contains(request.method) else { return false }
149✔
113

147✔
114
    if let statusCode = result.value?.statusCode, retryableHTTPStatusCodes.contains(statusCode) {
147✔
115
      return true
12✔
116
    }
135✔
117

135✔
118
    guard let errorCode = (result.error as? URLError)?.code else {
135✔
119
      return false
135✔
120
    }
135✔
121

×
122
    return retryableErrorCodes.contains(errorCode)
×
123
  }
149✔
124

125
  private func retry(
126
    _ request: HTTPRequest,
127
    retryCount: Int,
128
    next: @Sendable (HTTPRequest) async throws -> HTTPResponse
129
  ) async throws -> HTTPResponse {
161✔
130
    let result: Result<HTTPResponse, any Error>
161✔
131

161✔
132
    do {
161✔
133
      let response = try await next(request)
161✔
134
      result = .success(response)
161✔
135
    } catch {
161✔
136
      result = .failure(error)
×
137
    }
161✔
138

161✔
139
    if retryCount < retryLimit, shouldRetry(request: request, result: result) {
161✔
140
      let retryDelay =
12✔
141
        pow(
12✔
142
          Double(exponentialBackoffBase),
12✔
143
          Double(retryCount)
12✔
144
        ) * exponentialBackoffScale
12✔
145

12✔
146
      let nanoseconds = UInt64(retryDelay)
12✔
147
      try? await Task.sleep(nanoseconds: NSEC_PER_SEC * nanoseconds)
12✔
148

12✔
149
      if !Task.isCancelled {
12✔
150
        return try await retry(request, retryCount: retryCount + 1, next: next)
12✔
151
      }
12✔
152
    }
149✔
153

149✔
154
    return try result.get()
149✔
155
  }
161✔
156
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc