Skip to content

Commit 852a607

Browse files
authored
[swift6][client] Add new hooks to OpenAPIInterceptor (#22800)
1 parent 1f4017a commit 852a607

13 files changed

Lines changed: 835 additions & 230 deletions

File tree

modules/openapi-generator/src/main/resources/swift6/libraries/urlsession/URLSessionImplementations.mustache

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ fileprivate class URLSessionRequestBuilderConfiguration: @unchecked Sendable {
157157
let dataTask = urlSession.dataTaskFromProtocol(with: modifiedRequest) { data, response, error in
158158
self.cleanupRequest()
159159
160+
self.apiConfiguration.interceptor.didReceiveResponse(urlRequest: modifiedRequest, urlSession: urlSession, requestBuilder: self, data: data, response: response, error: error)
161+
160162
if let error = error {
161163
self.retryRequest(
162164
urlRequest: modifiedRequest,
@@ -196,7 +198,7 @@ fileprivate class URLSessionRequestBuilderConfiguration: @unchecked Sendable {
196198
return
197199
}
198200

199-
self.processRequestResponse(urlRequest: request, data: data, httpResponse: httpResponse, error: error, completion: completion)
201+
self.processRequestResponse(urlRequest: modifiedRequest, urlSession: urlSession, data: data, httpResponse: httpResponse, error: error, completion: completion)
200202
}
201203

202204
self.onProgressReady?(dataTask.progress)
@@ -205,15 +207,25 @@ fileprivate class URLSessionRequestBuilderConfiguration: @unchecked Sendable {
205207

206208
self.requestTask.set(task: dataTask)
207209

210+
self.apiConfiguration.interceptor.willSendRequest(urlRequest: modifiedRequest, urlSession: urlSession, requestBuilder: self)
211+
208212
dataTask.resume()
209213

210214
case .failure(let error):
215+
self.apiConfiguration.interceptor.didComplete(urlRequest: request, urlSession: urlSession, requestBuilder: self, data: nil, response: nil, result: .failure(error))
211216
self.apiConfiguration.apiResponseQueue.async {
212217
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
213218
}
214219
}
215220
}
216221
} catch {
222+
// Request creation failed - create a minimal request for error reporting
223+
let failedURL = URL(string: URLString) ?? URL(string: "about:blank")!
224+
var failedRequest = URLRequest(url: failedURL)
225+
failedRequest.httpMethod = method
226+
227+
self.apiConfiguration.interceptor.didComplete(urlRequest: failedRequest, urlSession: urlSession, requestBuilder: self, data: nil, response: nil, result: .failure(error))
228+
217229
self.apiConfiguration.apiResponseQueue.async {
218230
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
219231
}
@@ -235,19 +247,21 @@ fileprivate class URLSessionRequestBuilderConfiguration: @unchecked Sendable {
235247
self.execute(completion: completion)
236248
237249
case .dontRetry:
250+
self.apiConfiguration.interceptor.didComplete(urlRequest: urlRequest, urlSession: urlSession, requestBuilder: self, data: data, response: response, result: .failure(error))
238251
self.apiConfiguration.apiResponseQueue.async {
239252
completion(.failure(ErrorResponse.error(statusCode, data, response, error)))
240253
}
241254
}
242255
}
243256
}
244257

245-
fileprivate func processRequestResponse(urlRequest: URLRequest, data: Data?, httpResponse: HTTPURLResponse, error: Error?, completion: @Sendable @escaping (_ result: Swift.Result<Response<T>, ErrorResponse>) -> Void) {
258+
fileprivate func processRequestResponse(urlRequest: URLRequest, urlSession: URLSessionProtocol, data: Data?, httpResponse: HTTPURLResponse, error: Error?, completion: @Sendable @escaping (_ result: Swift.Result<Response<T>, ErrorResponse>) -> Void) {
246259
247260
switch T.self {
248261
case is Void.Type:
249-
250-
completion(.success(Response(response: httpResponse, body: () as! T, bodyData: data)))
262+
let result = () as! T
263+
apiConfiguration.interceptor.didComplete(urlRequest: urlRequest, urlSession: urlSession, requestBuilder: self, data: data, response: httpResponse, result: .success(result))
264+
completion(.success(Response(response: httpResponse, body: result, bodyData: data)))
251265
252266
default:
253267
fatalError("Unsupported Response Body Type - \(String(describing: T.self))")
@@ -320,18 +334,16 @@ fileprivate class URLSessionRequestBuilderConfiguration: @unchecked Sendable {
320334
}
321335

322336
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} class URLSessionDecodableRequestBuilder<T: Decodable & Sendable>: URLSessionRequestBuilder<T>, @unchecked Sendable {
323-
override fileprivate func processRequestResponse(urlRequest: URLRequest, data: Data?, httpResponse: HTTPURLResponse, error: Error?, completion: @Sendable @escaping (_ result: Swift.Result<Response<T>, ErrorResponse>) -> Void) {
337+
override fileprivate func processRequestResponse(urlRequest: URLRequest, urlSession: URLSessionProtocol, data: Data?, httpResponse: HTTPURLResponse, error: Error?, completion: @Sendable @escaping (_ result: Swift.Result<Response<T>, ErrorResponse>) -> Void) {
324338
325339
switch T.self {
326340
case is String.Type:
327-
328341
let body = data.flatMap { String(data: $0, encoding: .utf8) } ?? ""
329-
342+
apiConfiguration.interceptor.didComplete(urlRequest: urlRequest, urlSession: urlSession, requestBuilder: self, data: data, response: httpResponse, result: .success(body as! T))
330343
completion(.success(Response<T>(response: httpResponse, body: body as! T, bodyData: data)))
331344

332345
case is URL.Type:
333346
do {
334-
335347
guard error == nil else {
336348
throw DownloadException.responseFailed
337349
}
@@ -358,29 +370,37 @@ fileprivate class URLSessionRequestBuilderConfiguration: @unchecked Sendable {
358370
try fileManager.createDirectory(atPath: directoryPath, withIntermediateDirectories: true, attributes: nil)
359371
try data.write(to: filePath, options: .atomic)
360372

373+
apiConfiguration.interceptor.didComplete(urlRequest: urlRequest, urlSession: urlSession, requestBuilder: self, data: data, response: httpResponse, result: .success(filePath as! T))
361374
completion(.success(Response(response: httpResponse, body: filePath as! T, bodyData: data)))
362375

363376
} catch let requestParserError as DownloadException {
377+
apiConfiguration.interceptor.didComplete(urlRequest: urlRequest, urlSession: urlSession, requestBuilder: self, data: data, response: httpResponse, result: .failure(requestParserError))
364378
completion(.failure(ErrorResponse.error(400, data, httpResponse, requestParserError)))
365379
} catch {
380+
apiConfiguration.interceptor.didComplete(urlRequest: urlRequest, urlSession: urlSession, requestBuilder: self, data: data, response: httpResponse, result: .failure(error))
366381
completion(.failure(ErrorResponse.error(400, data, httpResponse, error)))
367382
}
368383

369384
case is Void.Type:
370-
371-
completion(.success(Response(response: httpResponse, body: () as! T, bodyData: data)))
385+
let result = () as! T
386+
apiConfiguration.interceptor.didComplete(urlRequest: urlRequest, urlSession: urlSession, requestBuilder: self, data: data, response: httpResponse, result: .success(result))
387+
completion(.success(Response(response: httpResponse, body: result, bodyData: data)))
372388

373389
case is Data.Type:
374-
375-
completion(.success(Response(response: httpResponse, body: data as! T, bodyData: data)))
390+
let result = data as! T
391+
apiConfiguration.interceptor.didComplete(urlRequest: urlRequest, urlSession: urlSession, requestBuilder: self, data: data, response: httpResponse, result: .success(result))
392+
completion(.success(Response(response: httpResponse, body: result, bodyData: data)))
376393

377394
default:
378-
379395
guard let unwrappedData = data, !unwrappedData.isEmpty else {
380396
if let expressibleByNilLiteralType = T.self as? ExpressibleByNilLiteral.Type {
381-
completion(.success(Response(response: httpResponse, body: expressibleByNilLiteralType.init(nilLiteral: ()) as! T, bodyData: data)))
397+
let result = expressibleByNilLiteralType.init(nilLiteral: ()) as! T
398+
apiConfiguration.interceptor.didComplete(urlRequest: urlRequest, urlSession: urlSession, requestBuilder: self, data: data, response: httpResponse, result: .success(result))
399+
completion(.success(Response(response: httpResponse, body: result, bodyData: data)))
382400
} else {
383-
completion(.failure(ErrorResponse.error(httpResponse.statusCode, nil, httpResponse, DecodableRequestBuilderError.emptyDataResponse)))
401+
let emptyDataError = DecodableRequestBuilderError.emptyDataResponse
402+
apiConfiguration.interceptor.didComplete(urlRequest: urlRequest, urlSession: urlSession, requestBuilder: self, data: nil, response: httpResponse, result: .failure(emptyDataError))
403+
completion(.failure(ErrorResponse.error(httpResponse.statusCode, nil, httpResponse, emptyDataError)))
384404
}
385405
return
386406
}
@@ -389,8 +409,10 @@ fileprivate class URLSessionRequestBuilderConfiguration: @unchecked Sendable {
389409

390410
switch decodeResult {
391411
case let .success(decodableObj):
412+
apiConfiguration.interceptor.didComplete(urlRequest: urlRequest, urlSession: urlSession, requestBuilder: self, data: unwrappedData, response: httpResponse, result: .success(decodableObj))
392413
completion(.success(Response(response: httpResponse, body: decodableObj, bodyData: unwrappedData)))
393414
case let .failure(error):
415+
apiConfiguration.interceptor.didComplete(urlRequest: urlRequest, urlSession: urlSession, requestBuilder: self, data: unwrappedData, response: httpResponse, result: .failure(error))
394416
completion(.failure(ErrorResponse.error(httpResponse.statusCode, unwrappedData, httpResponse, error)))
395417
}
396418
}
@@ -711,19 +733,47 @@ extension JSONDataEncoding: ParameterEncoding {}
711733
case dontRetry
712734
}
713735

714-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} protocol OpenAPIInterceptor {
736+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} protocol OpenAPIInterceptor: Sendable {
737+
// MARK: - Request Modification & Retry
738+
739+
/// Called before the request is sent. Allows modifying the URLRequest (e.g., adding authentication headers).
715740
func intercept<T>(urlRequest: URLRequest, urlSession: URLSessionProtocol, requestBuilder: RequestBuilder<T>, completion: @Sendable @escaping (Result<URLRequest, Error>) -> Void)
716741
742+
/// Called when a request fails. Allows the interceptor to decide whether to retry the request.
717743
func retry<T>(urlRequest: URLRequest, urlSession: URLSessionProtocol, requestBuilder: RequestBuilder<T>, data: Data?, response: URLResponse?, error: Error, completion: @Sendable @escaping (OpenAPIInterceptorRetry) -> Void)
744+
745+
// MARK: - Lifecycle Hooks
746+
747+
/// Called right before the request is sent, after all modifications from `intercept()` have been applied.
748+
/// Useful for logging the final request that will be sent.
749+
func willSendRequest<T>(urlRequest: URLRequest, urlSession: URLSessionProtocol, requestBuilder: RequestBuilder<T>)
750+
751+
/// Called when the raw response is received, before any processing or decoding.
752+
/// Useful for logging raw responses or performing custom validation.
753+
func didReceiveResponse<T>(urlRequest: URLRequest, urlSession: URLSessionProtocol, requestBuilder: RequestBuilder<T>, data: Data?, response: URLResponse?, error: Error?)
754+
755+
/// Called after the request completes (either success or failure).
756+
/// Useful for cleanup, analytics, or performance monitoring.
757+
func didComplete<T>(urlRequest: URLRequest, urlSession: URLSessionProtocol, requestBuilder: RequestBuilder<T>, data: Data?, response: URLResponse?, result: Result<T, Error>)
758+
}
759+
760+
// MARK: - Default Implementations (No-op)
761+
762+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} extension OpenAPIInterceptor {
763+
func willSendRequest<T>(urlRequest: URLRequest, urlSession: URLSessionProtocol, requestBuilder: RequestBuilder<T>) {}
764+
765+
func didReceiveResponse<T>(urlRequest: URLRequest, urlSession: URLSessionProtocol, requestBuilder: RequestBuilder<T>, data: Data?, response: URLResponse?, error: Error?) {}
766+
767+
func didComplete<T>(urlRequest: URLRequest, urlSession: URLSessionProtocol, requestBuilder: RequestBuilder<T>, data: Data?, response: URLResponse?, result: Result<T, Error>) {}
718768
}
719769

720-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} class DefaultOpenAPIInterceptor: OpenAPIInterceptor {
770+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} final class DefaultOpenAPIInterceptor: OpenAPIInterceptor {
721771
public init() {}
722-
772+
723773
public func intercept<T>(urlRequest: URLRequest, urlSession: URLSessionProtocol, requestBuilder: RequestBuilder<T>, completion: @Sendable @escaping (Result<URLRequest, any Error>) -> Void) {
724774
completion(.success(urlRequest))
725775
}
726-
776+
727777
public func retry<T>(urlRequest: URLRequest, urlSession: URLSessionProtocol, requestBuilder: RequestBuilder<T>, data: Data?, response: URLResponse?, error: Error, completion: @Sendable @escaping (OpenAPIInterceptorRetry) -> Void) {
728778
completion(.dontRetry)
729779
}

0 commit comments

Comments
 (0)