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

supabase / supabase-swift / 12913384967

22 Jan 2025 05:08PM UTC coverage: 69.815% (+21.5%) from 48.322%
12913384967

Pull #645

github

web-flow
Merge 82784a670 into 37a32aef8
Pull Request #645: test: integration tests revamp

95 of 134 new or added lines in 16 files covered. (70.9%)

37 existing lines in 8 files now uncovered.

4679 of 6702 relevant lines covered (69.81%)

13.81 hits per line

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

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

8
import Foundation
9
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 maximum number of retries.
34
  package let retryLimit: Int
35
  /// The base value for exponential backoff.
36
  package let exponentialBackoffBase: UInt
37
  /// The scale factor for exponential backoff.
38
  package let exponentialBackoffScale: Double
39
  /// The set of retryable HTTP methods.
40
  package let retryableHTTPMethods: Set<HTTPTypes.HTTPRequest.Method>
41
  /// The set of retryable HTTP status codes.
42
  package let retryableHTTPStatusCodes: Set<Int>
43
  /// The set of retryable URL error codes.
44
  package let retryableErrorCodes: Set<URLError.Code>
45

46
  /// Creates a `RetryRequestInterceptor` instance.
47
  ///
48
  /// - Parameters:
49
  ///   - retryLimit: The maximum number of retries. Default is `2`.
50
  ///   - exponentialBackoffBase: The base value for exponential backoff. Default is `2`.
51
  ///   - exponentialBackoffScale: The scale factor for exponential backoff. Default is `0.5`.
52
  ///   - retryableHTTPMethods: The set of retryable HTTP methods. Default includes common methods.
53
  ///   - retryableHTTPStatusCodes: The set of retryable HTTP status codes. Default includes common status codes.
54
  ///   - retryableErrorCodes: The set of retryable URL error codes. Default includes common error codes.
55
  package init(
56
    retryLimit: Int = RetryRequestInterceptor.defaultRetryLimit,
57
    exponentialBackoffBase: UInt = RetryRequestInterceptor.defaultExponentialBackoffBase,
58
    exponentialBackoffScale: Double = RetryRequestInterceptor.defaultExponentialBackoffScale,
59
    retryableHTTPMethods: Set<HTTPTypes.HTTPRequest.Method> = RetryRequestInterceptor.defaultRetryableHTTPMethods,
60
    retryableHTTPStatusCodes: Set<Int> = defaultRetryableHTTPStatusCodes,
61
    retryableErrorCodes: Set<URLError.Code> = defaultRetryableURLErrorCodes
62
  ) {
51✔
63
    precondition(
51✔
64
      exponentialBackoffBase >= 2, "The `exponentialBackoffBase` must be a minimum of 2."
51✔
65
    )
51✔
66

51✔
67
    self.retryLimit = retryLimit
51✔
68
    self.exponentialBackoffBase = exponentialBackoffBase
51✔
69
    self.exponentialBackoffScale = exponentialBackoffScale
51✔
70
    self.retryableHTTPMethods = retryableHTTPMethods
51✔
71
    self.retryableHTTPStatusCodes = retryableHTTPStatusCodes
51✔
72
    self.retryableErrorCodes = retryableErrorCodes
51✔
73
  }
51✔
74

75
  /// Intercepts an HTTP request and automatically retries it in case of failure.
76
  ///
77
  /// - Parameters:
78
  ///   - request: The original HTTP request to be intercepted and retried.
79
  ///   - next: A closure representing the next interceptor in the chain.
80
  /// - Returns: The HTTP response obtained after retrying.
81
  package func intercept(
82
    _ request: HTTPRequest,
83
    next: @Sendable (HTTPRequest) async throws -> HTTPResponse
84
  ) async throws -> HTTPResponse {
41✔
85
    try await retry(request, retryCount: 1, next: next)
41✔
86
  }
41✔
87

88
  private func shouldRetry(request: HTTPRequest, result: Result<HTTPResponse, any Error>) -> Bool {
41✔
89
    guard retryableHTTPMethods.contains(request.method) else { return false }
41✔
90

41✔
91
    if let statusCode = result.value?.statusCode, retryableHTTPStatusCodes.contains(statusCode) {
41✔
92
      return true
×
93
    }
41✔
94

41✔
95
    guard let errorCode = (result.error as? URLError)?.code else {
41✔
96
      return false
41✔
97
    }
41✔
98

×
99
    return retryableErrorCodes.contains(errorCode)
×
100
  }
41✔
101

102
  private func retry(
103
    _ request: HTTPRequest,
104
    retryCount: Int,
105
    next: @Sendable (HTTPRequest) async throws -> HTTPResponse
106
  ) async throws -> HTTPResponse {
41✔
107
    let result: Result<HTTPResponse, any Error>
41✔
108

41✔
109
    do {
41✔
110
      let response = try await next(request)
41✔
111
      result = .success(response)
41✔
112
    } catch {
41✔
UNCOV
113
      result = .failure(error)
×
114
    }
41✔
115

41✔
116
    if retryCount < retryLimit, shouldRetry(request: request, result: result) {
41✔
117
      let retryDelay = pow(
×
118
        Double(exponentialBackoffBase),
×
119
        Double(retryCount)
×
120
      ) * exponentialBackoffScale
×
121

×
122
      let nanoseconds = UInt64(retryDelay)
×
123
      try? await Task.sleep(nanoseconds: NSEC_PER_SEC * nanoseconds)
×
124

×
125
      if !Task.isCancelled {
×
126
        return try await retry(request, retryCount: retryCount + 1, next: next)
×
127
      }
×
128
    }
41✔
129

41✔
130
    return try result.get()
41✔
131
  }
41✔
132
}
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