Skip to content

Commit fb7fc3a

Browse files
committed
extract response
1 parent b672f8e commit fb7fc3a

File tree

3 files changed

+51
-40
lines changed

3 files changed

+51
-40
lines changed

Sources/SwiftAPIClient/Modifiers/ErrorHandler.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ public struct APIErrorContext: Equatable {
4545

4646
public var request: HTTPRequestComponents?
4747
public var response: Data?
48-
public var status: HTTPResponse.Status?
48+
49+
public var httpResponse: HTTPResponse?
50+
public var status: HTTPResponse.Status? { httpResponse?.status }
4951
public var codeLocation: CodeLocation
5052
public var fileID: String {
5153
get { codeLocation.fileID }
@@ -67,6 +69,7 @@ public struct APIErrorContext: Equatable {
6769
public init(
6870
request: HTTPRequestComponents? = nil,
6971
response: Data? = nil,
72+
httpResponse: HTTPResponse? = nil,
7073
status: HTTPResponse.Status? = nil,
7174
fileID: String,
7275
line: UInt,
@@ -75,6 +78,7 @@ public struct APIErrorContext: Equatable {
7578
self.init(
7679
request: request,
7780
response: response,
81+
httpResponse: httpResponse,
7882
status: status,
7983
codeLocation: CodeLocation(
8084
fileID: fileID,
@@ -87,25 +91,28 @@ public struct APIErrorContext: Equatable {
8791
public init(
8892
request: HTTPRequestComponents? = nil,
8993
response: Data? = nil,
94+
httpResponse: HTTPResponse? = nil,
9095
status: HTTPResponse.Status? = nil,
9196
codeLocation: CodeLocation
9297
) {
9398
self.request = request
99+
self.httpResponse = httpResponse
94100
self.response = response
95-
self.status = status
96101
self.codeLocation = codeLocation
97102
}
98103

99104
@available(*, deprecated, message: "Use init(request:response:status:codeLocation:) instead")
100105
public init(
101106
request: HTTPRequestComponents? = nil,
102107
response: Data? = nil,
108+
httpResponse: HTTPResponse? = nil,
103109
status: HTTPResponse.Status? = nil,
104110
fileIDLine: CodeLocation
105111
) {
106112
self.init(
107113
request: request,
108114
response: response,
115+
httpResponse: httpResponse,
109116
status: status,
110117
fileID: fileIDLine.fileID,
111118
line: fileIDLine.line,

Sources/SwiftAPIClient/Modifiers/HTTPResponseValidator.swift

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public extension HTTPResponseValidator {
3232
static func statusCode(_ codes: ClosedRange<Int>) -> Self {
3333
HTTPResponseValidator { response, _, configs in
3434
guard codes.contains(response.status.code) || configs.ignoreStatusCodeValidator else {
35-
throw InvalidStatusCode(response.status)
35+
throw InvalidStatusCode(response: response)
3636
}
3737
}
3838
}
@@ -43,25 +43,32 @@ public extension HTTPResponseValidator {
4343
static func statusCode(_ kind: HTTPResponse.Status.Kind) -> Self {
4444
HTTPResponseValidator { response, _, configs in
4545
guard response.status.kind == kind || configs.ignoreStatusCodeValidator else {
46-
throw InvalidStatusCode(response.status)
46+
throw InvalidStatusCode(response: response)
4747
}
4848
}
4949
}
5050
}
5151

52+
/// An error indicating that an HTTP response has an invalid status code.
5253
public struct InvalidStatusCode: Error, LocalizedError, CustomStringConvertible {
53-
54-
/// The invalid status code.
55-
public let status: HTTPResponse.Status
56-
57-
public init(_ status: HTTPResponse.Status) {
58-
self.status = status
59-
}
60-
61-
public var errorDescription: String? { description }
62-
public var description: String {
63-
"Invalid status code: \(status.code) \(status.reasonPhrase)"
64-
}
54+
55+
/// The invalid status code.
56+
public var status: HTTPResponse.Status { response.status }
57+
public let response: HTTPResponse
58+
59+
@available(*, deprecated, message: "Use init(response: HTTPResponse) instead")
60+
public init(_ status: HTTPResponse.Status) {
61+
self.init(response: HTTPResponse(status: status))
62+
}
63+
64+
public init(response: HTTPResponse) {
65+
self.response = response
66+
}
67+
68+
public var errorDescription: String? { description }
69+
public var description: String {
70+
"Invalid status code: \(status.code) \(status.reasonPhrase)"
71+
}
6572
}
6673

6774
public extension HTTPResponseValidator {
@@ -107,34 +114,30 @@ public extension APIClient.Configs {
107114
}
108115
}
109116

110-
func extractStatusCodeEvenFailed<T>(_ request: () async throws -> (T, HTTPResponse)) async throws -> (Result<(T, HTTPResponse), Error>, HTTPResponse.Status) {
111-
let status: HTTPResponse.Status
117+
func extractResponseEvenFailed<T>(_ request: () async throws -> (T, HTTPResponse)) async throws -> (Result<(T, HTTPResponse), Error>, HTTPResponse) {
118+
let response: HTTPResponse
112119
let result: Result<(T, HTTPResponse), Error>
113120
do {
114121
let value = try await request()
115-
status = value.1.status
122+
response = value.1
116123
result = .success(value)
117124
} catch let error as InvalidStatusCode {
118-
status = error.status
125+
response = error.response
119126
result = .failure(error)
120127
} catch let error as APIClientError {
121-
if let code = error.context.status {
122-
status = code
128+
if let httpResponse = error.context.httpResponse {
129+
response = httpResponse
123130
result = .failure(error)
124131
} else {
125132
throw error
126133
}
127134
} catch {
128135
throw error
129136
}
130-
return (result, status)
137+
return (result, response)
131138
}
132139

133-
func extractStatusCode(from error: Error) -> HTTPResponse.Status? {
134-
if let error = error as? InvalidStatusCode {
135-
return error.status
136-
} else if let error = error as? APIClientError {
137-
return error.context.status
138-
}
139-
return nil
140+
func extractStatusCodeEvenFailed<T>(_ request: () async throws -> (T, HTTPResponse)) async throws -> (Result<(T, HTTPResponse), Error>, HTTPResponse.Status) {
141+
let (result, response) = try await extractResponseEvenFailed(request)
142+
return (result, response.status)
140143
}

Sources/SwiftAPIClient/Modifiers/RetryModifier.swift

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ public struct RetryRequestCondition {
269269
}
270270

271271
/// A `RetryRequestCondition` that retries the request when the response status code indicates a failure that is typically transient.
272-
public static let retryStatusCode = RetryRequestCondition.statusCodes([408, 421, 429, 500, 502, 503, 504, 509])
272+
public static let retryStatusCode = RetryRequestCondition.statusCodes(408, 421, 429, 500, 502, 503, 504, 509)
273273

274274
/// A `RetryRequestCondition` that retries the request when the response status code is `429 Too Many Requests`.
275275
public static let rateLimitExceeded = RetryRequestCondition.statusCodes(.tooManyRequests)
@@ -344,7 +344,7 @@ private struct retryMiddleware: HTTPClientMiddleware {
344344
}
345345
}
346346
var count = 0
347-
var status: HTTPResponse.Status?
347+
var response: HTTPResponse?
348348
var retryAfterHeader: TimeInterval = 0
349349

350350
func needRetry(_ result: Result<HTTPResponse, Error>) -> Bool {
@@ -361,7 +361,7 @@ private struct retryMiddleware: HTTPClientMiddleware {
361361
if count > 0 {
362362
let interval = UInt64(max(retryAfterHeader, interval(count - 1)) * 1_000_000_000)
363363
if interval > 0 {
364-
if let status, let hash = backoffPolicy.scopeHash(request), backoffPolicy.isGlobalBackoff(request, status) {
364+
if let response, let hash = backoffPolicy.scopeHash(request), backoffPolicy.isGlobalBackoff(request, response.status) {
365365
_ = try await withThrowingSynchronizedAccess(id: hash) {
366366
try await Task.sleep(nanoseconds: interval)
367367
return interval
@@ -372,23 +372,24 @@ private struct retryMiddleware: HTTPClientMiddleware {
372372
}
373373
}
374374
count += 1
375-
let (result, sts) = try await extractStatusCodeEvenFailed {
375+
let (result, rsp) = try await extractResponseEvenFailed {
376376
try await next(request, configs)
377377
}
378-
status = sts
378+
response = rsp
379379
return try result.get()
380380
}
381381

382382
while true {
383383
do {
384-
let (data, response) = try await retry()
385-
if [429, 503].contains((status ?? response.status).code) {
386-
retryAfterHeader = response.headerFields[.retryAfter].flatMap {
384+
let (data, httpResponse) = try await retry()
385+
// 429 and 503 may include Retry-After header by RFC 7231
386+
if [429, 503].contains(httpResponse.status.code) {
387+
retryAfterHeader = httpResponse.headerFields[.retryAfter].flatMap {
387388
decodeRetryAfterHeader($0, formatter: configs.retryAfterHeaderDateFormatter)
388389
} ?? 0
389390
}
390-
if !needRetry(.success(response)) {
391-
return (data, response)
391+
if !needRetry(.success(httpResponse)) {
392+
return (data, httpResponse)
392393
}
393394
} catch {
394395
if !needRetry(.failure(error)) {

0 commit comments

Comments
 (0)