From bf58be03e2ce9582e23d81d452ca14c618b87e4c Mon Sep 17 00:00:00 2001 From: Eser Kucuker Date: Fri, 30 Jan 2026 22:57:34 +0300 Subject: [PATCH 1/2] Implement AsyncNetworkableError with localization and CustomNSError bridging, replace NSError throws in AsyncNetworkable+Data, add HTTP status to response logs, and add localized strings for network error keys. Add AsyncNetworkableError with localized descriptions and CustomNSError bridging to preserve domain/code/userInfo. Replace NSError throws in AsyncNetworkable+Data with enum-based errors, including httpError(statusCode:data:) for non-2xx responses. Improve response logging by including HTTP status text. Add localized strings for new network error keys in en.lproj and tr.lproj. --- .../AsyncNetworkable+Data.swift | 32 ++++------- .../AsyncNetworkableError.swift | 56 +++++++++++++++++++ .../Log/AsyncNetworkable+logs.swift | 14 ++++- .../Resources/en.lproj/Localizable.strings | 3 + .../Resources/tr.lproj/Localizable.strings | 3 + 5 files changed, 85 insertions(+), 23 deletions(-) create mode 100644 Sources/MBAsyncNetworking/AsyncNetworkableError.swift diff --git a/Sources/MBAsyncNetworking/AsyncNetworkable+Data.swift b/Sources/MBAsyncNetworking/AsyncNetworkable+Data.swift index b65b30e..82e44d0 100644 --- a/Sources/MBAsyncNetworking/AsyncNetworkable+Data.swift +++ b/Sources/MBAsyncNetworking/AsyncNetworkable+Data.swift @@ -35,7 +35,7 @@ public extension AsyncNetworkable { isRefreshToken: isRefreshToken ) case 200 ... 299: - printResponse(data, request: request) + printResponse(data, request: request, response: httpResponse) do { if T.self is MBEmptyCodable.Type { // swiftlint:disable force_cast @@ -47,7 +47,11 @@ public extension AsyncNetworkable { return data as! T // swiftlint:enable force_cast } - return try decoder.decode(T.self, from: data) + if data.isEmpty, httpResponse.statusCode == 204 { + throw AsyncNetworkableError.noContent + } + let response = try decoder.decode(T.self, from: data) + return response } catch { let error = NSError( domain: "", @@ -58,10 +62,9 @@ public extension AsyncNetworkable { throw error } default: - let error = NSError( - domain: "MBAsyncNetworking", - code: httpResponse.statusCode, - userInfo: ["MBAsyncNetworkingErrorData": data] + let error = AsyncNetworkableError.httpError( + statusCode: httpResponse.statusCode, + data: data ) printErrorLog(error, request: request) throw error @@ -84,13 +87,7 @@ public extension AsyncNetworkable { isRefreshToken: Bool ) async throws -> T { guard hasAuthentication else { - throw NSError( - domain: "MBAsyncNetworking", - code: 401, - userInfo: [ - NSLocalizedDescriptionKey: "unauthorized" - ] - ) + throw AsyncNetworkableError.unauthorized } if !isRefreshToken { return try await RequestQueue.shared.executeAfterTokenRefresh { @@ -101,14 +98,7 @@ public extension AsyncNetworkable { } } else { await UserSession.clear() - throw NSError( - domain: "MBAsyncNetworking", - code: -3, - userInfo: [ - NSLocalizedDescriptionKey: "Tokenization could not be completed because, " + - "access_token and/or exires_in is not in expected format." - ] - ) + throw AsyncNetworkableError.tokenizationFailed } } // swiftformat:enable all diff --git a/Sources/MBAsyncNetworking/AsyncNetworkableError.swift b/Sources/MBAsyncNetworking/AsyncNetworkableError.swift new file mode 100644 index 0000000..fe7885e --- /dev/null +++ b/Sources/MBAsyncNetworking/AsyncNetworkableError.swift @@ -0,0 +1,56 @@ +// +// AsyncNetworkableError.swift +// MBAsyncNetworking +// +// Prompted by Eser Küçüker using Cursor on 30.01.2026. +// + +import Foundation + +enum AsyncNetworkableError: Error { + case noContent + case unauthorized + case tokenizationFailed + case httpError(statusCode: Int, data: Data) +} + +extension AsyncNetworkableError: LocalizedError { + var errorDescription: String? { + switch self { + case .noContent: + NSLocalizedString("network.error.noContent", bundle: .module, comment: "") + case .unauthorized: + NSLocalizedString("network.error.unauthorized", bundle: .module, comment: "") + case .tokenizationFailed: + NSLocalizedString("network.error.tokenizationFailed", bundle: .module, comment: "") + case .httpError: + nil + } + } +} + +extension AsyncNetworkableError: CustomNSError { + static var errorDomain: String { "MBAsyncNetworking" } + + var errorCode: Int { + switch self { + case .noContent: + return 204 + case .unauthorized: + return 401 + case .tokenizationFailed: + return -3 + case let .httpError(statusCode, _): + return statusCode + } + } + + var errorUserInfo: [String: Any] { + switch self { + case let .httpError(_, data): + return ["MBAsyncNetworkingErrorData": data] + default: + return [NSLocalizedDescriptionKey: errorDescription ?? ""] + } + } +} diff --git a/Sources/MBAsyncNetworking/Log/AsyncNetworkable+logs.swift b/Sources/MBAsyncNetworking/Log/AsyncNetworkable+logs.swift index ca1f34b..0ed85e1 100644 --- a/Sources/MBAsyncNetworking/Log/AsyncNetworkable+logs.swift +++ b/Sources/MBAsyncNetworking/Log/AsyncNetworkable+logs.swift @@ -18,8 +18,18 @@ public extension AsyncNetworkable { /// - Parameters: /// - data: The response data to log. Can be `nil`. /// - request: The original URL request associated with the response. - func printResponse(_ data: Data?, request: URLRequest) { - let logString = getRequestLog(request) + "\n" + getStringFrom(data) + /// - response: The HTTP response received for the request. + func printResponse(_ data: Data?, request: URLRequest, response: HTTPURLResponse?) { + var logString = getRequestLog(request) + if let response { + logString.append("\n") + logString.append( + "Status: \(response.statusCode) " + + HTTPURLResponse.localizedString(forStatusCode: response.statusCode) + ) + } + logString.append("\n") + logString.append(getStringFrom(data)) printLog(logString) NetworkLogsManager.shared.delegate?.didReceiveResponse( request: request, diff --git a/Sources/MBAsyncNetworking/Resources/en.lproj/Localizable.strings b/Sources/MBAsyncNetworking/Resources/en.lproj/Localizable.strings index f1af3ae..a1f843e 100644 --- a/Sources/MBAsyncNetworking/Resources/en.lproj/Localizable.strings +++ b/Sources/MBAsyncNetworking/Resources/en.lproj/Localizable.strings @@ -2,3 +2,6 @@ auth.error.missingToken = "Authentication failed: No token available."; auth.error.refreshingFailed = "Token refresh failed. Unable to obtain a new authentication token."; auth.error.storageNotFound = "Token storage is unavailable. Ensure the authentication storage mechanism is correctly implemented."; auth.error.tokenQueueResumeFailed = "Failed to resume queued request after token refresh. Please retry."; +network.error.noContent = "No Content"; +network.error.unauthorized = "Unauthorized"; +network.error.tokenizationFailed = "Tokenization could not be completed because access_token and/or exires_in is not in expected format."; diff --git a/Sources/MBAsyncNetworking/Resources/tr.lproj/Localizable.strings b/Sources/MBAsyncNetworking/Resources/tr.lproj/Localizable.strings index c2e62cb..7b85600 100644 --- a/Sources/MBAsyncNetworking/Resources/tr.lproj/Localizable.strings +++ b/Sources/MBAsyncNetworking/Resources/tr.lproj/Localizable.strings @@ -2,3 +2,6 @@ auth.error.missingToken = "Kimlik doğrulama başarısız: Token bulunamadı."; auth.error.refreshingFailed = "Token yenileme başarısız oldu. Yeni kimlik doğrulama tokenı alınamadı."; auth.error.storageNotFound = "Token saklama mekanizması bulunamadı. Kimlik doğrulama depolama sisteminin düzgün çalıştığından emin olun."; auth.error.tokenQueueResumeFailed = "Token yenilendikten sonra sıraya alınan istek devam ettirilemedi. Lütfen tekrar deneyin."; +network.error.noContent = "İçerik yok"; +network.error.unauthorized = "Yetkisiz"; +network.error.tokenizationFailed = "Tokenizasyon tamamlanamadı çünkü access_token ve/veya exires_in beklenen formatta değil."; From a331951371111b0050afc579be15cddd50dbc34b Mon Sep 17 00:00:00 2001 From: Eser Kucuker Date: Fri, 30 Jan 2026 23:21:48 +0300 Subject: [PATCH 2/2] swiftFormat 6.0 --- .../AsyncNetworkable+Data.swift | 3 +-- .../AsyncNetworkableError.swift | 16 +++++++++------- .../Log/AsyncNetworkable+logs.swift | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Sources/MBAsyncNetworking/AsyncNetworkable+Data.swift b/Sources/MBAsyncNetworking/AsyncNetworkable+Data.swift index 82e44d0..8d08e09 100644 --- a/Sources/MBAsyncNetworking/AsyncNetworkable+Data.swift +++ b/Sources/MBAsyncNetworking/AsyncNetworkable+Data.swift @@ -50,8 +50,7 @@ public extension AsyncNetworkable { if data.isEmpty, httpResponse.statusCode == 204 { throw AsyncNetworkableError.noContent } - let response = try decoder.decode(T.self, from: data) - return response + return try decoder.decode(T.self, from: data) } catch { let error = NSError( domain: "", diff --git a/Sources/MBAsyncNetworking/AsyncNetworkableError.swift b/Sources/MBAsyncNetworking/AsyncNetworkableError.swift index fe7885e..c3b1dcf 100644 --- a/Sources/MBAsyncNetworking/AsyncNetworkableError.swift +++ b/Sources/MBAsyncNetworking/AsyncNetworkableError.swift @@ -30,27 +30,29 @@ extension AsyncNetworkableError: LocalizedError { } extension AsyncNetworkableError: CustomNSError { - static var errorDomain: String { "MBAsyncNetworking" } + static var errorDomain: String { + "MBAsyncNetworking" + } var errorCode: Int { switch self { case .noContent: - return 204 + 204 case .unauthorized: - return 401 + 401 case .tokenizationFailed: - return -3 + -3 case let .httpError(statusCode, _): - return statusCode + statusCode } } var errorUserInfo: [String: Any] { switch self { case let .httpError(_, data): - return ["MBAsyncNetworkingErrorData": data] + ["MBAsyncNetworkingErrorData": data] default: - return [NSLocalizedDescriptionKey: errorDescription ?? ""] + [NSLocalizedDescriptionKey: errorDescription ?? ""] } } } diff --git a/Sources/MBAsyncNetworking/Log/AsyncNetworkable+logs.swift b/Sources/MBAsyncNetworking/Log/AsyncNetworkable+logs.swift index 0ed85e1..d53e9cb 100644 --- a/Sources/MBAsyncNetworking/Log/AsyncNetworkable+logs.swift +++ b/Sources/MBAsyncNetworking/Log/AsyncNetworkable+logs.swift @@ -25,7 +25,7 @@ public extension AsyncNetworkable { logString.append("\n") logString.append( "Status: \(response.statusCode) " + - HTTPURLResponse.localizedString(forStatusCode: response.statusCode) + HTTPURLResponse.localizedString(forStatusCode: response.statusCode) ) } logString.append("\n")