Skip to content

Commit 700841d

Browse files
committed
fix status validation error
1 parent 02f82ae commit 700841d

12 files changed

+135
-25
lines changed

Sources/SwiftAPIClient/Clients/HTTPClient.swift

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,15 +117,19 @@ extension APIClientCaller where Result == AsyncThrowingValue<(Value, HTTPRespons
117117
let value: T
118118
let response: HTTPResponse
119119
let start = Date()
120+
let responseWrapper = SendableValue<(T, HTTPResponse)?>(nil)
120121
let requestWrapper = SendableValue(request)
121122
do {
122123
(value, response) = try await configs.httpClientMiddleware.execute(request: request, configs: configs) { request, configs in
123124
configs.logRequest(request, uuid: uuid)
124125
await requestWrapper.set(request)
125-
return try await task(request, configs)
126+
let result = try await task(request, configs)
127+
await responseWrapper.set(result)
128+
return result
126129
}
127130
} catch {
128131
let request = await requestWrapper.value
132+
let response = await responseWrapper.value
129133
let duration = Date().timeIntervalSince(start)
130134
if !configs._errorLoggingComponents.isEmpty {
131135
let message = configs._errorLoggingComponents.errorMessage(
@@ -139,10 +143,19 @@ extension APIClientCaller where Result == AsyncThrowingValue<(Value, HTTPRespons
139143
}
140144
#if canImport(Metrics)
141145
if configs.reportMetrics {
142-
updateHTTPMetrics(for: request, status: nil, duration: duration, successful: false)
146+
updateHTTPMetrics(for: request, status: response?.1.status, duration: duration, successful: false)
143147
}
144148
#endif
145-
try configs.errorHandler(error, configs, APIErrorContext(request: request, fileIDLine: configs.fileIDLine ?? FileIDLine()))
149+
try configs.errorHandler(
150+
error,
151+
configs,
152+
APIErrorContext(
153+
request: request,
154+
response: response.flatMap { data($0.0) },
155+
status: response?.1.status,
156+
fileIDLine: configs.fileIDLine ?? FileIDLine()
157+
)
158+
)
146159
throw error
147160
}
148161
let request = await requestWrapper.value

Sources/SwiftAPIClient/Modifiers/BackgroundModifiers.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ private struct BackgroundTaskMiddleware: HTTPClientMiddleware {
2121
func execute<T>(
2222
request: HTTPRequestComponents,
2323
configs: APIClient.Configs,
24-
next: @escaping @Sendable (HTTPRequestComponents, APIClient.Configs) async throws -> (T, HTTPResponse)
24+
next: @escaping Next<T>
2525
) async throws -> (T, HTTPResponse) {
2626
let id = await UIApplication.shared.beginBackgroundTask(
2727
withName: "Background Task for \(request.url?.absoluteString ?? "")"
@@ -45,7 +45,7 @@ private struct RetryOnEnterForegroundMiddleware: HTTPClientMiddleware {
4545
func execute<T>(
4646
request: HTTPRequestComponents,
4747
configs: APIClient.Configs,
48-
next: @escaping @Sendable (HTTPRequestComponents, APIClient.Configs) async throws -> (T, HTTPResponse)
48+
next: @escaping Next<T>
4949
) async throws -> (T, HTTPResponse) {
5050
func makeRequest() async throws -> (T, HTTPResponse) {
5151
let wasInBackground = WasInBackgroundService()

Sources/SwiftAPIClient/Modifiers/HTTPClientMiddleware.swift

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@ import Foundation
22

33
public protocol HTTPClientMiddleware {
44

5+
//#if swift(>=6.0)
6+
// typealias Next<T> = @Sendable (HTTPRequestComponents, APIClient.Configs) async throws(HTTPClientMiddlewareError) -> (T, HTTPResponse)
7+
//#else
8+
typealias Next<T> = @Sendable (HTTPRequestComponents, APIClient.Configs) async throws -> (T, HTTPResponse)
9+
//#endif
10+
511
func execute<T>(
612
request: HTTPRequestComponents,
713
configs: APIClient.Configs,
8-
next: @escaping @Sendable (HTTPRequestComponents, APIClient.Configs) async throws -> (T, HTTPResponse)
14+
next: @escaping Next<T>
915
) async throws -> (T, HTTPResponse)
1016
}
1117

@@ -26,6 +32,52 @@ public extension APIClient.Configs {
2632
}
2733
}
2834

35+
struct HTTPClientMiddlewareError: Error {
36+
37+
public let underlying: Error
38+
private var values: [PartialKeyPath<Self>: Any] = [:]
39+
40+
public init(_ underlying: Error) {
41+
self.underlying = underlying
42+
}
43+
44+
public init(_ underlying: String) {
45+
self.underlying = Errors.custom(underlying)
46+
}
47+
48+
public subscript<T>(_ keyPath: WritableKeyPath<Self, T>) -> T? {
49+
get {
50+
values[keyPath] as? T
51+
}
52+
set {
53+
values[keyPath] = newValue
54+
}
55+
}
56+
57+
public subscript<T>(_ keyPath: WritableKeyPath<Self, T?>) -> T? {
58+
get {
59+
values[keyPath] as? T
60+
}
61+
set {
62+
values[keyPath] = newValue
63+
}
64+
}
65+
66+
public func with<T>(_ keyPath: WritableKeyPath<Self, T>, _ value: T) -> HTTPClientMiddlewareError {
67+
var result = self
68+
result[keyPath: keyPath] = value
69+
return result
70+
}
71+
}
72+
73+
extension HTTPClientMiddlewareError {
74+
75+
var response: HTTPResponse? {
76+
get { self[\.response] }
77+
set { self[\.response] = newValue }
78+
}
79+
}
80+
2981
private extension APIClient.Configs {
3082

3183
var httpClientArrayMiddleware: HTTPClientArrayMiddleware {
@@ -36,17 +88,31 @@ private extension APIClient.Configs {
3688

3789
private struct HTTPClientArrayMiddleware: HTTPClientMiddleware {
3890

91+
typealias AnyNext<T> = @Sendable (HTTPRequestComponents, APIClient.Configs) async throws -> (T, HTTPResponse)
92+
3993
var middlewares: [HTTPClientMiddleware] = []
4094

4195
func execute<T>(
4296
request: HTTPRequestComponents,
4397
configs: APIClient.Configs,
44-
next: @escaping @Sendable (HTTPRequestComponents, APIClient.Configs) async throws -> (T, HTTPResponse)
98+
next: @escaping Next<T>
4599
) async throws -> (T, HTTPResponse) {
46100
var next = next
47101
for middleware in middlewares {
48102
next = { [next] in try await middleware.execute(request: $0, configs: $1, next: next) }
49103
}
50104
return try await next(request, configs)
51105
}
106+
107+
private func wrapNext<T>(_ next: @escaping AnyNext<T>) -> Next<T> {
108+
{ request, configs in
109+
do {
110+
return try await next(request, configs)
111+
} catch let error as HTTPClientMiddlewareError {
112+
throw error
113+
} catch {
114+
throw HTTPClientMiddlewareError(error)
115+
}
116+
}
117+
}
52118
}

Sources/SwiftAPIClient/Modifiers/HTTPResponseValidator.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Foundation
22
#if canImport(FoundationNetworking)
33
import FoundationNetworking
44
#endif
5+
import HTTPTypes
56

67
/// A struct for validating HTTP responses.
78
public struct HTTPResponseValidator {
@@ -105,3 +106,26 @@ public extension APIClient.Configs {
105106
set { self[\.ignoreStatusCodeValidator] = newValue }
106107
}
107108
}
109+
110+
func extractStatusCodeEvenFailed<T>(_ request: () async throws -> (T, HTTPResponse)) async throws -> (Result<(T, HTTPResponse), Error>, HTTPResponse.Status) {
111+
let status: HTTPResponse.Status
112+
let result: Result<(T, HTTPResponse), Error>
113+
do {
114+
let value = try await request()
115+
status = value.1.status
116+
result = .success(value)
117+
} catch let error as InvalidStatusCode {
118+
status = error.status
119+
result = .failure(error)
120+
} catch let error as APIClientError {
121+
if let code = error.context.status {
122+
status = code
123+
result = .failure(error)
124+
} else {
125+
throw error
126+
}
127+
} catch {
128+
throw error
129+
}
130+
return (result, status)
131+
}

Sources/SwiftAPIClient/Modifiers/RateLimitModifier.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,22 +47,26 @@ private struct RateLimitMiddleware<ID: Hashable>: HTTPClientMiddleware {
4747
func execute<T>(
4848
request: HTTPRequestComponents,
4949
configs: APIClient.Configs,
50-
next: @escaping @Sendable (HTTPRequestComponents, APIClient.Configs) async throws -> (T, HTTPResponse)
50+
next: @escaping Next<T>
5151
) async throws -> (T, HTTPResponse) {
5252
let id = id(request)
5353
await waitForSynchronizedAccess(id: id, of: Void.self)
54-
var res = try await next(request, configs)
54+
var (res, status) = try await extractStatusCodeEvenFailed {
55+
try await next(request, configs)
56+
}
5557
var count: UInt = 0
5658
while
57-
statusCodes.contains(res.1.status),
59+
statusCodes.contains(status),
5860
count < maxCount
5961
{
6062
count += 1
6163
try await withThrowingSynchronizedAccess(id: id) {
6264
try await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000))
6365
}
64-
res = try await next(request, configs)
66+
(res, status) = try await extractStatusCodeEvenFailed {
67+
try await next(request, configs)
68+
}
6569
}
66-
return res
70+
return try res.get()
6771
}
6872
}

Sources/SwiftAPIClient/Modifiers/RequestCompression.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ private struct CompressionMiddleware: HTTPClientMiddleware {
4141
func execute<T>(
4242
request: HTTPRequestComponents,
4343
configs: APIClient.Configs,
44-
next: @escaping @Sendable (HTTPRequestComponents, APIClient.Configs) async throws -> (T, HTTPResponse)
44+
next: @escaping Next<T>
4545
) async throws -> (T, HTTPResponse) {
4646
// No need to compress unless we have body data. No support for compressing streams.
4747
guard let body = request.body else {

Sources/SwiftAPIClient/Modifiers/RetryModifier.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Foundation
22
#if canImport(FoundationNetworking)
33
import FoundationNetworking
44
#endif
5+
import HTTPTypes
56

67
public extension APIClient {
78

@@ -18,7 +19,7 @@ private struct RetryMiddleware: HTTPClientMiddleware {
1819
func execute<T>(
1920
request: HTTPRequestComponents,
2021
configs: APIClient.Configs,
21-
next: @escaping @Sendable (HTTPRequestComponents, APIClient.Configs) async throws -> (T, HTTPResponse)
22+
next: @escaping Next<T>
2223
) async throws -> (T, HTTPResponse) {
2324
var count = 0
2425
func needRetry() -> Bool {

Sources/SwiftAPIClient/Modifiers/ThrottleModifier.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ private struct RequestsThrottleMiddleware<ID: Hashable>: HTTPClientMiddleware {
6464
func execute<T>(
6565
request: HTTPRequestComponents,
6666
configs: APIClient.Configs,
67-
next: @escaping @Sendable (HTTPRequestComponents, APIClient.Configs) async throws -> (T, HTTPResponse)
67+
next: @escaping Next<T>
6868
) async throws -> (T, HTTPResponse) {
6969
let interval = configs.throttleInterval
7070
guard interval > 0 else {

Sources/SwiftAPIClient/Modifiers/TimeoutModifiers.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ private struct TimeoutMiddleware<D>: HTTPClientMiddleware {
159159
func execute<T>(
160160
request: HTTPRequestComponents,
161161
configs: APIClient.Configs,
162-
next: @escaping @Sendable (HTTPRequestComponents, APIClient.Configs) async throws -> (T, HTTPResponse)
162+
next: @escaping Next<T>
163163
) async throws -> (T, HTTPResponse) {
164164
try await withTimeout(
165165
timeout,

Sources/SwiftAPIClient/Modifiers/TokenRefresher/TokenRefresher.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ public struct TokenRefresherMiddleware: HTTPClientMiddleware {
122122
public func execute<T>(
123123
request: HTTPRequestComponents,
124124
configs: APIClient.Configs,
125-
next: @escaping @Sendable (HTTPRequestComponents, APIClient.Configs) async throws -> (T, HTTPResponse)
125+
next: @escaping Next<T>
126126
) async throws -> (T, HTTPResponse) {
127127
guard configs.isAuthEnabled else {
128128
return try await next(request, configs)
@@ -154,14 +154,16 @@ public struct TokenRefresherMiddleware: HTTPClientMiddleware {
154154
}
155155
var authorizedRequest = request
156156
try auth(accessToken).modifier(&authorizedRequest, configs)
157-
let result = try await next(authorizedRequest, configs)
158-
if expiredStatusCodes.contains(result.1.status) {
157+
let (result, status) = try await extractStatusCodeEvenFailed {
158+
try await next(authorizedRequest, configs)
159+
}
160+
if expiredStatusCodes.contains(status) {
159161
(accessToken, refreshToken, _) = try await refreshTokenAndCache(configs, accessToken: accessToken, refreshToken: refreshToken)
160162
authorizedRequest = request
161163
try auth(accessToken).modifier(&authorizedRequest, configs)
162164
return try await next(authorizedRequest, configs)
163165
}
164-
return result
166+
return try result.get()
165167
}
166168

167169
private func refreshTokenAndCache(

0 commit comments

Comments
 (0)