Skip to content

Commit d7a1f7c

Browse files
authored
Merge pull request #58 from GoodRequest/fix/error-logging
fix: Log error message without network response
2 parents f8501fd + 68d22bb commit d7a1f7c

5 files changed

Lines changed: 119 additions & 27 deletions

File tree

Sources/GoodNetworking/Logging/DataTaskLogging.swift

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,21 @@ internal extension DataTaskProxy {
3131
}
3232

3333
@NetworkActor private func prepareResponseStatus(response: URLResponse?, error: (any Error)?) -> String {
34-
guard let response = response as? HTTPURLResponse else { return "" }
34+
var errorMessage: String?
35+
if error != nil {
36+
errorMessage = "🚨 Error: \(error?.localizedDescription ?? "<nil>")"
37+
}
38+
39+
guard let response = response as? HTTPURLResponse else {
40+
return errorMessage ?? "⁉️ Response not received, error not available."
41+
}
3542
let statusCode = response.statusCode
3643

3744
var logMessage = (200 ..< 300).contains(statusCode) ? "\(statusCode): " : "\(statusCode): "
3845
logMessage.append(HTTPURLResponse.localizedString(forStatusCode: statusCode))
3946

40-
if error != nil {
41-
logMessage.append("\n🚨 Error: \(error?.localizedDescription ?? "<nil>")")
47+
if let errorMessage {
48+
logMessage.append("\n\(errorMessage)")
4249
}
4350

4451
return logMessage
@@ -54,7 +61,7 @@ internal extension DataTaskProxy {
5461
}
5562

5663
@NetworkActor private func prettyPrintMessage(data: Data?, mimeType: String? = "text/plain") -> String {
57-
guard let data else { return "" }
64+
guard let data, !data.isEmpty else { return "" }
5865
guard plainTextMimeTypeHeuristic(mimeType) else { return "🏞️ Detected MIME type is not plain text" }
5966
guard data.count < Self.maxLogSizeBytes else {
6067
return "💡 Data size is too big (\(data.count) bytes), console limit is \(Self.maxLogSizeBytes) bytes"

Sources/GoodNetworking/Models/Endpoint.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,20 +143,20 @@ public enum EndpointParameters {
143143

144144
// MARK: - Compatibility
145145

146-
@available(*, deprecated)
146+
@available(*, deprecated, message: "Encoding will be automatically determined by the kind of `parameters` in the future.")
147147
public protocol ParameterEncoding {}
148148

149-
@available(*, deprecated)
149+
@available(*, deprecated, message: "Encoding will be automatically determined by the kind of `parameters` in the future.")
150150
public enum URLEncoding: ParameterEncoding {
151151
case `default`
152152
}
153153

154-
@available(*, deprecated)
154+
@available(*, deprecated, message: "Encoding will be automatically determined by the kind of `parameters` in the future.")
155155
public enum JSONEncoding: ParameterEncoding {
156156
case `default`
157157
}
158158

159-
@available(*, deprecated)
159+
@available(*, deprecated, message: "Encoding will be automatically determined by the kind of `parameters` in the future.")
160160
public enum AutomaticEncoding: ParameterEncoding {
161161
case `default`
162162
}

Sources/GoodNetworking/Models/JSON.swift

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ import Foundation
7171

7272
// MARK: - Initializers
7373

74-
/// Create JSON from raw `Data`
74+
/// Create JSON from raw `Data`.
75+
///
7576
/// - Parameters:
7677
/// - data: Raw `Data` of JSON object
7778
/// - options: Optional serialization options
@@ -90,8 +91,9 @@ import Foundation
9091
/// - model: `Encodable` model
9192
/// - encoder: Encoder for encoding the model
9293
public init(encodable model: any Encodable, encoder: JSONEncoder) {
93-
if let data = try? encoder.encode(model), let converted = try? JSON(data: data) {
94-
self = converted
94+
if let data = try? encoder.encode(model),
95+
let jsonData = try? JSON(data: data) {
96+
self = jsonData
9597
} else {
9698
self = JSON.null
9799
}
@@ -105,20 +107,20 @@ import Foundation
105107
///
106108
/// - Parameter object: Object to try to represent as JSON
107109
public init(_ object: Any) {
108-
if let data = object as? Data, let converted = try? JSON(data: data) {
109-
self = converted
110-
} else if let model = object as? any Encodable, let data = try? JSONEncoder().encode(model), let converted = try? JSON(data: data) {
111-
self = converted
110+
if let data = object as? Data, let jsonData = try? JSON(data: data) {
111+
self = jsonData
112+
} else if let model = object as? any Encodable, let data = try? JSONEncoder().encode(model), let jsonData = try? JSON(data: data) {
113+
self = jsonData
112114
} else if let dictionary = object as? [String: Any] {
113115
self = JSON.dictionary(dictionary.mapValues { JSON($0) })
114116
} else if let array = object as? [Any] {
115117
self = JSON.array(array.map { JSON($0) })
116118
} else if let string = object as? String {
117119
self = JSON.string(string)
118-
} else if let bool = object as? Bool {
119-
self = JSON.bool(bool)
120120
} else if let number = object as? NSNumber {
121121
self = JSON.number(number)
122+
} else if let bool = object as? Bool {
123+
self = JSON.bool(bool)
122124
} else if let json = object as? JSON {
123125
self = json
124126
} else {
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//
2+
// NetworkResponse.swift
3+
// GoodNetworking
4+
//
5+
// Created by Filip Šašala on 30/11/2025.
6+
//
7+
8+
import Foundation
9+
10+
/// Wraps the payload returned from `URLSession` with
11+
/// metadata describing the HTTP response.
12+
public struct NetworkResponse: Sendable {
13+
14+
/// Raw body returned by the server.
15+
public let body: Data
16+
17+
/// Original `URLResponse` instance for advanced access when needed.
18+
public let urlResponse: URLResponse?
19+
20+
/// HTTP headers resolved and stored eagerly for concurrency safety.
21+
public let headers: HTTPHeaders
22+
23+
/// HTTP specific response, if available.
24+
public var httpResponse: HTTPURLResponse? {
25+
urlResponse as? HTTPURLResponse
26+
}
27+
28+
/// Final URL of the response.
29+
public var url: URL? {
30+
urlResponse?.url
31+
}
32+
33+
/// MIME type announced by the server.
34+
public var mimeType: String? {
35+
urlResponse?.mimeType
36+
}
37+
38+
/// Expected length of the body.
39+
public var expectedContentLength: Int64 {
40+
urlResponse?.expectedContentLength ?? -1 // NSURLResponseUnknownLength
41+
}
42+
43+
/// Text encoding specified by the response.
44+
public var textEncodingName: String? {
45+
urlResponse?.textEncodingName
46+
}
47+
48+
/// Suggested filename inferred by Foundation.
49+
public var suggestedFilename: String? {
50+
urlResponse?.suggestedFilename
51+
}
52+
53+
/// HTTP status code (or `-1` when not available).
54+
public var statusCode: Int {
55+
httpResponse?.statusCode ?? -1
56+
}
57+
58+
/// Raw header dictionary exposed without additional processing.
59+
public var allHeaderFields: [AnyHashable: Any]? {
60+
httpResponse?.allHeaderFields
61+
}
62+
63+
internal init(data: Data, response: URLResponse?) {
64+
self.body = data
65+
self.urlResponse = response
66+
67+
// decode HTTP headers if possible
68+
if let httpResponse = response as? HTTPURLResponse {
69+
var flattened: [String: String] = [:]
70+
httpResponse.allHeaderFields.forEach { header in
71+
guard let key = header.key as? String else { return }
72+
flattened[key] = String(describing: header.value)
73+
}
74+
self.headers = HTTPHeaders(flattened)
75+
} else {
76+
self.headers = HTTPHeaders([:])
77+
}
78+
}
79+
80+
}

Sources/GoodNetworking/Session/NetworkSession.swift

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ extension NetworkSessionDelegate: URLSessionDelegate {
180180

181181
case .deny(let reason):
182182
networkSession.getLogger().logNetworkEvent(
183-
message: reason,
183+
message: reason ?? "Denied for unspecified reasons",
184184
level: .error,
185185
file: #file,
186186
line: #line
@@ -270,7 +270,8 @@ extension NetworkSession {
270270
}
271271

272272
public func request<T: Decodable>(endpoint: Endpoint) async throws(NetworkError) -> T {
273-
let data = try await request(endpoint: endpoint) as Data
273+
let response: NetworkResponse = try await request(endpoint: endpoint)
274+
let data = response.body
274275

275276
// handle decoding corner cases
276277
var decoder = JSONDecoder()
@@ -306,8 +307,8 @@ extension NetworkSession {
306307

307308
@_disfavoredOverload
308309
public func request(endpoint: Endpoint) async throws(NetworkError) -> JSON {
309-
let responseData = try await request(endpoint: endpoint) as Data
310-
guard let json = try? JSON(data: responseData) else {
310+
let response: NetworkResponse = try await request(endpoint: endpoint)
311+
guard let json = try? JSON(data: response.body) else {
311312
throw URLError(.cannotDecodeRawData).asNetworkError()
312313
}
313314
return json
@@ -316,7 +317,7 @@ extension NetworkSession {
316317
// MARK: Raw
317318

318319
@discardableResult
319-
public func request(endpoint: Endpoint) async throws(NetworkError) -> Data {
320+
public func request(endpoint: Endpoint) async throws(NetworkError) -> NetworkResponse {
320321
let endpointPath = await endpoint.path.resolveUrl()
321322
let url: URL
322323

@@ -413,7 +414,7 @@ extension NetworkSession {
413414

414415
private extension NetworkSession {
415416

416-
func executeRequest(request: inout URLRequest) async throws(NetworkError) -> Data {
417+
func executeRequest(request: inout URLRequest) async throws(NetworkError) -> NetworkResponse {
417418
// Content type
418419
let httpMethodSupportsBody = request.method.hasRequestBody
419420
let httpMethodHasBody = (request.httpBody != nil)
@@ -450,17 +451,19 @@ private extension NetworkSession {
450451
do {
451452
let data = try await dataTaskProxy.data()
452453
closeProxyForTask(dataTask)
453-
454+
454455
let validator = DefaultValidationProvider()
455-
let statusCode = (dataTask.response as? HTTPURLResponse)?.statusCode ?? -1
456+
let response = dataTask.response
457+
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
458+
456459
try validator.validate(statusCode: statusCode, data: data)
457-
return data
460+
return NetworkResponse(data: data, response: response)
458461
} catch let networkError {
459462
return try await retryRequest(request: &request, error: networkError)
460463
}
461464
}
462465

463-
func retryRequest(request: inout URLRequest, error networkError: NetworkError) async throws(NetworkError) -> Data {
466+
func retryRequest(request: inout URLRequest, error networkError: NetworkError) async throws(NetworkError) -> NetworkResponse {
464467
let retryResult = try await interceptor.retry(urlRequest: &request, for: self, dueTo: networkError)
465468

466469
switch retryResult {

0 commit comments

Comments
 (0)