Skip to content

Commit

Permalink
Merge pull request #25 from loay-ashraf/feature/decode_http_error_body
Browse files Browse the repository at this point in the history
add decoding capability in case of failure caused due to HTTP status code
  • Loading branch information
loay-ashraf authored Aug 21, 2023
2 parents 2ae99c2 + 82aaae3 commit a2029db
Show file tree
Hide file tree
Showing 12 changed files with 106 additions and 54 deletions.
10 changes: 9 additions & 1 deletion RxNetworkKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
0B77E0BB29D968DE0077FBC0 /* RxRelay in Frameworks */ = {isa = PBXBuildFile; productRef = 0B77E0BA29D968DE0077FBC0 /* RxRelay */; };
0B77E0BD29D968DE0077FBC0 /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0B77E0BC29D968DE0077FBC0 /* RxSwift */; };
0B77E0C029D969370077FBC0 /* RxSwiftExt in Frameworks */ = {isa = PBXBuildFile; productRef = 0B77E0BF29D969370077FBC0 /* RxSwiftExt */; };
C6A9BEF62A93E2AE00459E32 /* DefaultHTTPErrorBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A9BEF52A93E2AE00459E32 /* DefaultHTTPErrorBody.swift */; };
C6A9BEF82A93E2D600459E32 /* HTTPErrorBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A9BEF72A93E2D600459E32 /* HTTPErrorBody.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand Down Expand Up @@ -108,6 +110,8 @@
0B77E08629D965D30077FBC0 /* NetworkDownloadRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkDownloadRouter.swift; sourceTree = "<group>"; };
0B77E08729D965D30077FBC0 /* NetworkRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkRouter.swift; sourceTree = "<group>"; };
0B77E08829D965D30077FBC0 /* NetworkUploadRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkUploadRouter.swift; sourceTree = "<group>"; };
C6A9BEF52A93E2AE00459E32 /* DefaultHTTPErrorBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultHTTPErrorBody.swift; sourceTree = "<group>"; };
C6A9BEF72A93E2D600459E32 /* HTTPErrorBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPErrorBody.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -215,11 +219,13 @@
0B77E06129D965D30077FBC0 /* HTTP */ = {
isa = PBXGroup;
children = (
C6A9BEF52A93E2AE00459E32 /* DefaultHTTPErrorBody.swift */,
C6A9BEF72A93E2D600459E32 /* HTTPErrorBody.swift */,
0B77E06229D965D30077FBC0 /* HTTPMIMEType.swift */,
0B77E06329D965D30077FBC0 /* HTTPMethod.swift */,
0B77E06429D965D30077FBC0 /* HTTPScheme.swift */,
0B77E06529D965D30077FBC0 /* HTTPURLResponse+StatusCode.swift */,
0B77E06629D965D30077FBC0 /* HTTPStatusCode.swift */,
0B77E06529D965D30077FBC0 /* HTTPURLResponse+StatusCode.swift */,
);
path = HTTP;
sourceTree = "<group>";
Expand Down Expand Up @@ -416,6 +422,7 @@
0B77E0B529D965D30077FBC0 /* NetworkRouter.swift in Sources */,
0B77E09529D965D30077FBC0 /* RequestRetryPolicy.swift in Sources */,
0B77E0B329D965D30077FBC0 /* Reactive+Curl.swift in Sources */,
C6A9BEF82A93E2D600459E32 /* HTTPErrorBody.swift in Sources */,
0B77E0A929D965D30077FBC0 /* NetworkError.swift in Sources */,
0B77E09F29D965D30077FBC0 /* NWPath+InterfaceType.swift in Sources */,
0B77E09C29D965D30077FBC0 /* HTTPScheme.swift in Sources */,
Expand All @@ -442,6 +449,7 @@
0B77E08B29D965D30077FBC0 /* URLSession+DownloadTask.swift in Sources */,
0B77E0A729D965D30077FBC0 /* NetworkServerError.swift in Sources */,
0B77E0B629D965D30077FBC0 /* NetworkUploadRouter.swift in Sources */,
C6A9BEF62A93E2AE00459E32 /* DefaultHTTPErrorBody.swift in Sources */,
0B77E0AB29D965D30077FBC0 /* Single+Retry.swift in Sources */,
0B77E08F29D965D30077FBC0 /* URLSession+UploadTask.swift in Sources */,
0B77E08C29D965D30077FBC0 /* Reactive+URLSessionAdaptedDownloadResponse.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,19 @@ extension Reactive where Base: URLSession {
///
/// - Parameters:
/// - request: `URLRequest` used to create upload task and its observables.
/// - httpErrorType: `HTTPErrorBody` http error body type.
/// - apiErrorType: `NetworkAPIError` type for expected error in HTTP response body.
///
/// - Returns: a `Observable` object of `DownloadEvent` type.
func downloadResponse<AE: NetworkAPIError>(request: URLRequest, apiErrorType: AE.Type) -> Observable<DownloadEvent> {
func downloadResponse<E: HTTPErrorBody, AE: NetworkAPIError>(request: URLRequest, httpErrorType: E.Type, apiErrorType: AE.Type) -> Observable<DownloadEvent> {
let observables = downloadResponse(request: request)
let progressObservable = observables
.0
.map { DownloadEvent.progress(progress: $0) }
.asObservable()
let responseObservable = observables
.1
.decodable(AE.self)
.decodable(E.self, apiErrorType: AE.self)
.map { DownloadEvent.completedWithData(data: $0.data) }
.asObservable()
let mergedObservable = responseObservable.merge(with: progressObservable)
Expand All @@ -36,18 +37,19 @@ extension Reactive where Base: URLSession {
/// - Parameters:
/// - request: `URLRequest` used to create upload task and its observables.
/// - url: `URL` used to save downloaded file to disk.
/// - httpErrorType: `HTTPErrorBody` http error body type.
/// - apiErrorType: `NetworkAPIError` type for expected error in HTTP response body.
///
/// - Returns: a `Observable` object of `DownloadEvent` type.
func downloadResponse<AE: NetworkAPIError>(request: URLRequest, saveTo url: URL, apiErrorType: AE.Type) -> Observable<DownloadEvent> {
func downloadResponse<E: HTTPErrorBody, AE: NetworkAPIError>(request: URLRequest, saveTo url: URL, httpErrorType: E.Type, apiErrorType: AE.Type) -> Observable<DownloadEvent> {
let observables = downloadResponse(request: request, saveTo: url)
let progressObservable = observables
.0
.map { DownloadEvent.progress(progress: $0) }
.asObservable()
let responseObservable = observables
.1
.decodable(AE.self)
.decodable(E.self, apiErrorType: AE.self)
.map { _ in DownloadEvent.completed }
.asObservable()
let mergedObservable = responseObservable.merge(with: progressObservable)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,19 @@ extension Reactive where Base: URLSession {
/// - request: `URLRequest` used to create upload task and its observables.
/// - file: `UploadFile` object to be uploaded.
/// - modelType: `Decodable` type for model in HTTP response body.
/// - httpErrorType: `HTTPErrorBody` http error body type.
/// - apiErrorType: `NetworkAPIError` type for expected error in HTTP response body.
///
/// - Returns: a `Observable` object of `UploadEvent` type.
func uploadResponse<T: Decodable, AE: NetworkAPIError>(request: URLRequest, file: UploadFile, modelType: T.Type, apiErrorType: AE.Type) -> Observable<UploadEvent<T>> {
func uploadResponse<T: Decodable, E: HTTPErrorBody, AE: NetworkAPIError>(request: URLRequest, file: UploadFile, modelType: T.Type, httpErrorType: E.Type, apiErrorType: AE.Type) -> Observable<UploadEvent<T>> {
let observables = uploadResponse(request: request, file: file)
let progressObservable = observables
.0
.map { UploadEvent<T>.progress(progress: $0) }
.asObservable()
let responseObservable = observables
.1
.decodable(T.self, errorType: AE.self)
.decodable(T.self, httpErrorType: E.self, apiErrorType: AE.self)
.map { UploadEvent<T>.completed(model: $0) }
.asObservable()
let mergedObservable = responseObservable.merge(with: progressObservable)
Expand All @@ -39,18 +40,19 @@ extension Reactive where Base: URLSession {
/// - request: `URLRequest` used to create upload task and its observables.
/// - formData: `UploadFormData` object that includes parameters and files to be uploaded.
/// - modelType: `Decodable` type for model in HTTP response body.
/// - httpErrorType: `HTTPErrorBody` http error body type.
/// - apiErrorType: `NetworkAPIError` type for expected error in HTTP response body.
///
/// - Returns: a `Observable` object of `UploadEvent` type.
func uploadResponse<T: Decodable, AE: NetworkAPIError>(request: URLRequest, formData: UploadFormData, modelType: T.Type, apiErrorType: AE.Type) -> Observable<UploadEvent<T>> {
func uploadResponse<T: Decodable, E: HTTPErrorBody, AE: NetworkAPIError>(request: URLRequest, formData: UploadFormData, modelType: T.Type, httpErrorType: E.Type, apiErrorType: AE.Type) -> Observable<UploadEvent<T>> {
let observables = uploadResponse(request: request, formData: formData)
let progressObservable = observables
.0
.map { UploadEvent<T>.progress(progress: $0) }
.asObservable()
let responseObservable = observables
.1
.decodable(T.self, errorType: AE.self)
.decodable(T.self, httpErrorType: E.self, apiErrorType: AE.self)
.map { UploadEvent<T>.completed(model: $0) }
.asObservable()
let mergedObservable = responseObservable.merge(with: progressObservable)
Expand Down
2 changes: 1 addition & 1 deletion Source/Error/NetworkClientError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

/// Client-side (transport) error
public enum NetworkClientError: Error {
case http(HTTPStatusCode)
case http(HTTPStatusCode, HTTPErrorBody?)
case serialization(Error)
case transport(Error)
}
13 changes: 9 additions & 4 deletions Source/Error/NetworkError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,20 @@ public enum NetworkError: Error {
/// Creates `NetworkError` instance.
///
/// - Parameter response: `HTTPURLResponse` used to get response status code.
init?(_ response: HTTPURLResponse?) {
init?<E: HTTPErrorBody>(_ response: HTTPURLResponse?, data: Data?, errorType: E.Type) {
if let response = response,
let httpStatusCode = HTTPStatusCode(rawValue: response.statusCode) {
let httpStatusCode = response.status {
// Get Error body from response data if possible.
var httpErrorBody: E?
if let data = data {
httpErrorBody = try? JSONDecoder().decode(errorType.self, from: data)
}
// Get Error from response status code
switch httpStatusCode.responseType {
case .clientError:
self = .client(.http(httpStatusCode))
self = .client(.http(httpStatusCode, httpErrorBody))
case .serverError:
self = .server(.http(httpStatusCode))
self = .server(.http(httpStatusCode, httpErrorBody))
default:
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion Source/Error/NetworkServerError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@

/// Server-side error
public enum NetworkServerError: Error {
case http(HTTPStatusCode)
case http(HTTPStatusCode, HTTPErrorBody?)
case generic(Error)
}
14 changes: 14 additions & 0 deletions Source/HTTP/DefaultHTTPErrorBody.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// DefaultHTTPErrorBody.swift
// RxNetworkKit
//
// Created by Loay Ashraf on 21/08/2023.
//

import Foundation

struct DefaultHTTPErrorBody: HTTPErrorBody {
let statusCode: Int?
let message: String?
let supportId: String?
}
10 changes: 10 additions & 0 deletions Source/HTTP/HTTPErrorBody.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// HTTPErrorBody.swift
// RxNetworkKit
//
// Created by Loay Ashraf on 21/08/2023.
//

import Foundation

public protocol HTTPErrorBody: Decodable { }
30 changes: 18 additions & 12 deletions Source/Manager/NetworkManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ public class NetworkManager {
///
/// - Parameters:
/// - router: `Router` object used to create request.
/// - httpErrorType: `HTTPErrorBody` http error body type.
/// - apiErrorType: `NetworkAPIError` type expected to be received in response body.
///
/// - Returns: `Completable` observable encapsulating data request.
public func request<AE: NetworkAPIError>(_ router: NetworkRouter, _ apiErrorType: AE.Type = DefaultNetworkAPIError.self) -> Completable {
public func request<E: HTTPErrorBody, AE: NetworkAPIError>(_ router: NetworkRouter, _ httpErrorType: E.Type = DefaultHTTPErrorBody.self, _ apiErrorType: AE.Type = DefaultNetworkAPIError.self) -> Completable {

let originalRequest = router.asURLRequest()
let adaptedRequest = requestInterceptor.adapt(originalRequest, for: session)
Expand All @@ -45,7 +46,7 @@ public class NetworkManager {
let observable = session
.rx
.response(request: adaptedRequest)
.decodable(AE.self)
.decodable(E.self, apiErrorType: AE.self)
.retry(retryMaxAttempts, delay: retryPolicy, shouldRetry: shouldRetry)
return observable
}
Expand All @@ -54,10 +55,11 @@ public class NetworkManager {
///
/// - Parameters:
/// - router: `Router` object used to create request.
/// - httpErrorType: `HTTPErrorBody` http error body type.
/// - apiErrorType: `NetworkAPIError` type expected to be received in response body.
///
/// - Returns: `Single` observable encapsulating data request.
public func request<T: Decodable, AE: NetworkAPIError>(_ router: NetworkRouter, _ apiErrorType: AE.Type = DefaultNetworkAPIError.self) -> Single<T> {
public func request<T: Decodable, E: HTTPErrorBody, AE: NetworkAPIError>(_ router: NetworkRouter, _ httpErrorType: E.Type = DefaultHTTPErrorBody.self, _ apiErrorType: AE.Type = DefaultNetworkAPIError.self) -> Single<T> {
let originalRequest = router.asURLRequest()
let adaptedRequest = requestInterceptor.adapt(originalRequest, for: session)
let retryMaxAttempts = requestInterceptor.retryMaxAttempts(adaptedRequest, for: session)
Expand All @@ -68,7 +70,7 @@ public class NetworkManager {
let observable = session
.rx
.response(request: adaptedRequest)
.decodable(T.self, errorType: AE.self)
.decodable(T.self, httpErrorType: E.self, apiErrorType: AE.self)
.retry(retryMaxAttempts, delay: retryPolicy, shouldRetry: shouldRetry)
return observable
}
Expand All @@ -77,10 +79,11 @@ public class NetworkManager {
///
/// - Parameters:
/// - router: `Router` object used to create request.
/// - httpErrorType: `HTTPErrorBody` http error body type.
/// - apiErrorType: `NetworkAPIError` type expected to be received in response body.
///
/// - Returns: `Observable` object encapsulating download request.
public func download<AE: NetworkAPIError>(_ router: NetworkRouter, _ apiErrorType: AE.Type = DefaultNetworkAPIError.self) -> Observable<DownloadEvent> {
public func download<E: HTTPErrorBody, AE: NetworkAPIError>(_ router: NetworkRouter, _ httpErrorType: E.Type = DefaultHTTPErrorBody.self, _ apiErrorType: AE.Type = DefaultNetworkAPIError.self) -> Observable<DownloadEvent> {
let originalRequest = router.asURLRequest()
let adaptedRequest = requestInterceptor.adapt(originalRequest, for: session)
let retryMaxAttempts = requestInterceptor.retryMaxAttempts(adaptedRequest, for: session)
Expand All @@ -90,7 +93,7 @@ public class NetworkManager {
}
let observable = session
.rx
.downloadResponse(request: adaptedRequest, apiErrorType: AE.self)
.downloadResponse(request: adaptedRequest, httpErrorType: E.self, apiErrorType: AE.self)
.retry(retryMaxAttempts, delay: retryPolicy, shouldRetry: shouldRetry)
return observable
}
Expand All @@ -100,10 +103,11 @@ public class NetworkManager {
/// - Parameters:
/// - router: `Router` object used to create request.
/// - fileURL: `URL` used to save downloaded file to disk.
/// - httpErrorType: `HTTPErrorBody` http error body type.
/// - apiErrorType: `NetworkAPIError` type expected to be received in response body.
///
/// - Returns: `Observable` object encapsulating download request.
public func download<AE: NetworkAPIError>(_ router: NetworkRouter, _ fileURL: URL, _ apiErrorType: AE.Type = DefaultNetworkAPIError.self) -> Observable<DownloadEvent> {
public func download<E: HTTPErrorBody, AE: NetworkAPIError>(_ router: NetworkRouter, _ fileURL: URL, _ httpErrorType: E.Type = DefaultHTTPErrorBody.self, _ apiErrorType: AE.Type = DefaultNetworkAPIError.self) -> Observable<DownloadEvent> {
let originalRequest = router.asURLRequest()
let adaptedRequest = requestInterceptor.adapt(originalRequest, for: session)
let retryMaxAttempts = requestInterceptor.retryMaxAttempts(adaptedRequest, for: session)
Expand All @@ -113,7 +117,7 @@ public class NetworkManager {
}
let observable = session
.rx
.downloadResponse(request: adaptedRequest, saveTo: fileURL, apiErrorType: AE.self)
.downloadResponse(request: adaptedRequest, saveTo: fileURL, httpErrorType: E.self, apiErrorType: AE.self)
.retry(retryMaxAttempts, delay: retryPolicy, shouldRetry: shouldRetry)
return observable
}
Expand All @@ -123,10 +127,11 @@ public class NetworkManager {
/// - Parameters:
/// - router: `Router` object used to create request.
/// - file: `UploadFile` object including file details for upload.
/// - httpErrorType: `HTTPErrorBody` http error body type.
/// - apiErrorType: `NetworkAPIError` type expected to be received in response body.
///
/// - Returns: `Observable` object encapsulating upload request.
public func upload<T: Decodable, AE: NetworkAPIError>(_ router: NetworkUploadRouter, _ file: UploadFile, _ apiErrorType: AE.Type = DefaultNetworkAPIError.self) -> Observable<UploadEvent<T>> {
public func upload<T: Decodable, E: HTTPErrorBody, AE: NetworkAPIError>(_ router: NetworkUploadRouter, _ file: UploadFile, _ httpErrorType: E.Type = DefaultHTTPErrorBody.self, _ apiErrorType: AE.Type = DefaultNetworkAPIError.self) -> Observable<UploadEvent<T>> {
let originalRequest = router.asURLRequest()
let adaptedRequest = requestInterceptor.adapt(originalRequest, for: session)
let retryMaxAttempts = requestInterceptor.retryMaxAttempts(adaptedRequest, for: session)
Expand All @@ -136,7 +141,7 @@ public class NetworkManager {
}
let observable = session
.rx
.uploadResponse(request: adaptedRequest, file: file, modelType: T.self, apiErrorType: AE.self)
.uploadResponse(request: adaptedRequest, file: file, modelType: T.self, httpErrorType: E.self, apiErrorType: AE.self)
.retry(retryMaxAttempts, delay: retryPolicy, shouldRetry: shouldRetry)
return observable
}
Expand All @@ -146,10 +151,11 @@ public class NetworkManager {
/// - Parameters:
/// - router: `Router` object used to create request.
/// - formData: `UploadFormData` object including parameters and files for upload.
/// - httpErrorType: `HTTPErrorBody` http error body type.
/// - apiErrorType: `NetworkAPIError` type expected to be received in response body.
///
/// - Returns: `Observable` object encapsulating upload request.
public func upload<T: Decodable, AE: NetworkAPIError>(_ router: NetworkUploadRouter, _ formData: UploadFormData, _ apiErrorType: AE.Type = DefaultNetworkAPIError.self) -> Observable<UploadEvent<T>> {
public func upload<T: Decodable, E: HTTPErrorBody, AE: NetworkAPIError>(_ router: NetworkUploadRouter, _ formData: UploadFormData, _ httpErrorType: E.Type = DefaultHTTPErrorBody.self, _ apiErrorType: AE.Type = DefaultNetworkAPIError.self) -> Observable<UploadEvent<T>> {
let originalRequest = router.asURLRequest()
let adaptedRequest = requestInterceptor.adapt(originalRequest, for: session)
let retryMaxAttempts = requestInterceptor.retryMaxAttempts(adaptedRequest, for: session)
Expand All @@ -159,7 +165,7 @@ public class NetworkManager {
}
let observable = session
.rx
.uploadResponse(request: adaptedRequest, formData: formData, modelType: T.self, apiErrorType: AE.self)
.uploadResponse(request: adaptedRequest, formData: formData, modelType: T.self, httpErrorType: E.self, apiErrorType: AE.self)
.retry(retryMaxAttempts, delay: retryPolicy, shouldRetry: shouldRetry)
return observable
}
Expand Down
Loading

0 comments on commit a2029db

Please sign in to comment.