@@ -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