Added the rate limit middleware.

This commit is contained in:
javier committed 2026-10-25 18:14:19 +01:00
1 parent 35b3b7953d
commit 2d86dff608
4 files changed
+326

No files matched your search

@@ -63,6 +63,17 @@ repeat {
} while cursor != nil
```
### Handling Rate Limits
Add ``RateLimitMiddleware`` before the authentication middleware to retry `429 Too Many Requests` responses:
```swift
middlewares: [
RateLimitMiddleware(),
BearerAuthMiddleware { try await tokenProvider.token() }
]
```
## Topics
### API Client
@@ -75,6 +86,10 @@ repeat {
- ``BearerAuthMiddleware``
- ``APIKeyTokenProvider``
### Rate Limiting
- ``RateLimitMiddleware``
### Generated Types
- ``Components``
@@ -0,0 +1,149 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the ASConnectService open source project
//
// Copyright (c) 2026 Röck+Cöde VoF. and the ASConnectService project authors
// Licensed under Apache license v2.0
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of ASConnectService project authors
//
// SPDX-License-Identifier: Apache-2.0
//
// ===----------------------------------------------------------------------===
import struct Foundation.URL
import typealias Foundation.TimeInterval
import struct HTTPTypes.HTTPRequest
import struct HTTPTypes.HTTPResponse
import protocol OpenAPIRuntime.ClientMiddleware
import class OpenAPIRuntime.HTTPBody
/// A client middleware that retries rate-limited requests.
///
/// On a `429 Too Many Requests` response, it waits for the `Retry-After` seconds, or a delay that doubles from
/// `baseDelay`, and retries. It returns the `429` response instead when the retries run out, the delay exceeds
/// `maximumDelay`, or the request body can only be sent once.
///
/// ## Usage
///
/// Place it before ``BearerAuthMiddleware``, so retries get a fresh token:
///
/// ```swift
/// middlewares: [
/// RateLimitMiddleware(),
/// BearerAuthMiddleware { try await tokenProvider.token() }
/// ]
/// ```
public struct RateLimitMiddleware {
// MARK: Properties
/// The maximum number of times a request is retried.
private let maximumRetries: Int
/// The delay before the first retry, when the response has no `Retry-After` header.
private let baseDelay: TimeInterval
/// The longest delay to wait before a retry.
private let maximumDelay: TimeInterval
/// The closure that waits for a given number of seconds.
private let sleep: @Sendable (TimeInterval) async throws -> Void
// MARK: Initializers
/// Creates a new middleware instance.
/// - Parameters:
/// - maximumRetries: The maximum number of times a request is retried.
/// - baseDelay: The delay, in seconds, before the first retry when the response has no `Retry-After` header.
/// - maximumDelay: The longest delay, in seconds, to wait before a retry.
public init(
maximumRetries: Int = 3,
baseDelay: TimeInterval = 1,
maximumDelay: TimeInterval = 60
) {
self.init(
maximumRetries: maximumRetries,
baseDelay: baseDelay,
maximumDelay: maximumDelay,
sleep: { try await Task.sleep(nanoseconds: UInt64($0 * 1_000_000_000)) }
)
}
/// Creates a new middleware instance that waits with `sleep`, for testing.
init(
maximumRetries: Int,
baseDelay: TimeInterval,
maximumDelay: TimeInterval,
sleep: @escaping @Sendable (TimeInterval) async throws -> Void
) {
self.maximumRetries = maximumRetries
self.baseDelay = baseDelay
self.maximumDelay = maximumDelay
self.sleep = sleep
}
}
// MARK: - ClientMiddleware
extension RateLimitMiddleware: ClientMiddleware {
// MARK: Methods
/// Sends an HTTP request, and retries it while its response is `429 Too Many Requests`.
/// - Parameters:
/// - request: The original HTTP request.
/// - body: The optional body of the request.
/// - baseURL: The base URL for the request.
/// - operationID: The identifier of the API operation being performed.
/// - next: The next handler in the middleware chain.
/// - Returns: The HTTP response and optional body returned by the next handler.
/// - Throws: An error if the next handler throws, or the task is cancelled while waiting.
public func intercept(
_ request: HTTPRequest,
body: HTTPBody?,
baseURL: URL,
operationID: String,
next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?)
) async throws -> (HTTPResponse, HTTPBody?) {
var retries = 0
var backoff = baseDelay
while true {
let (response, responseBody) = try await next(
request,
body,
baseURL
)
guard
response.status == .tooManyRequests,
retries < maximumRetries,
body?.iterationBehavior != .single
else {
return (response, responseBody)
}
let delay = response.retryAfter ?? backoff
guard delay <= maximumDelay else {
return (response, responseBody)
}
try await sleep(delay)
retries += 1
backoff *= 2
}
}
}
// MARK: - HTTPResponse+RetryAfter
private extension HTTPResponse {
/// The seconds in the `Retry-After` header, if any, and never negative.
var retryAfter: TimeInterval? {
headerFields[.retryAfter]
.flatMap { TimeInterval($0) }
.map { max($0, 0) }
}
}