Skip to content

Commit 87b0bb2

Browse files
author
Šimon Šesták
committed
refactor(networkobserver): Clarify callback lifecycle
Explicitly document that `didReceiveResponse` is always invoked with raw data, and `didFail` is an additional callback for errors occurring after the response is received. Update tests to assert this behavior.
1 parent c0f4980 commit 87b0bb2

2 files changed

Lines changed: 25 additions & 19 deletions

File tree

Sources/FTAPIKit/NetworkObserver.swift

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,27 @@ import FoundationNetworking
77
/// Protocol for observing network request lifecycle events.
88
///
99
/// Implement this protocol to add logging, analytics, or request tracking.
10-
/// The `context` parameter allows passing correlation data (request ID, start time, etc.)
11-
/// between `willSendRequest` and the completion callbacks.
10+
///
11+
/// ## Context Lifecycle
12+
/// The `Context` associated type allows passing correlation data (request ID, start time, etc.)
13+
/// through the request lifecycle:
14+
/// 1. `willSendRequest` is called before the request starts and returns a `Context` value
15+
/// 2. `didReceiveResponse` is always called with the raw response data (useful for debugging)
16+
/// 3. `didFail` is called additionally if the request processing fails (network, HTTP status, or decoding error)
17+
/// 4. If the observer is deallocated before the request completes, the context is discarded
18+
/// and no completion callback is invoked
1219
public protocol NetworkObserver: AnyObject, Sendable {
1320
associatedtype Context: Sendable
1421

1522
/// Called immediately before a request is sent.
1623
/// - Parameter request: The URLRequest about to be sent
17-
/// - Returns: Context to be passed to `didReceiveResponse` or `didFail`
24+
/// - Returns: Context to be passed to `didReceiveResponse` and optionally `didFail`
1825
func willSendRequest(_ request: URLRequest) -> Context
1926

20-
/// Called when a response is received.
27+
/// Called when a response is received from the server.
28+
///
29+
/// This is always called with the raw response data, even if processing subsequently fails.
30+
/// This allows observers to inspect the actual response for debugging purposes.
2131
/// - Parameters:
2232
/// - request: The original request
2333
/// - response: The URL response (may be HTTPURLResponse)
@@ -26,9 +36,11 @@ public protocol NetworkObserver: AnyObject, Sendable {
2636
func didReceiveResponse(for request: URLRequest, response: URLResponse?, data: Data?, context: Context)
2737

2838
/// Called when a request fails with an error.
39+
///
40+
/// Called after `didReceiveResponse` if processing determines the request failed.
2941
/// - Parameters:
3042
/// - request: The original request
31-
/// - error: The error that occurred
43+
/// - error: The error that occurred (may be network, HTTP status, or decoding error)
3244
/// - context: Value returned from `willSendRequest`
3345
func didFail(request: URLRequest, error: Error, context: Context)
3446
}

Tests/FTAPIKitTests/NetworkObserverTests.swift

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,8 @@ final class NetworkObserverTests: XCTestCase {
5050
wait(for: [expectation], timeout: timeout)
5151

5252
XCTAssertEqual(mockObserver.willSendCount, 1, "willSendRequest should be called once")
53-
XCTAssertGreaterThanOrEqual(
54-
mockObserver.didReceiveCount + mockObserver.didFailCount,
55-
1,
56-
"Either didReceive or didFail should be called"
57-
)
53+
// didReceiveResponse is always called; didFail is called additionally on failure
54+
XCTAssertEqual(mockObserver.didReceiveCount, 1, "didReceiveResponse should always be called")
5855
}
5956

6057
func testObserverLogsFailedRequest() {
@@ -69,13 +66,10 @@ final class NetworkObserverTests: XCTestCase {
6966

7067
wait(for: [expectation], timeout: timeout)
7168

72-
// Verify observer was called (request is always logged, even on failure)
73-
XCTAssertEqual(mockObserver.willSendCount, 1, "Request should be logged once")
74-
XCTAssertGreaterThanOrEqual(
75-
mockObserver.didReceiveCount + mockObserver.didFailCount,
76-
1,
77-
"Either response or error should be logged"
78-
)
69+
// didReceiveResponse is always called with raw data; didFail is called additionally on failure
70+
XCTAssertEqual(mockObserver.willSendCount, 1, "willSendRequest should be called once")
71+
XCTAssertEqual(mockObserver.didReceiveCount, 1, "didReceiveResponse should always be called")
72+
XCTAssertEqual(mockObserver.didFailCount, 1, "didFail should be called on failure")
7973
}
8074

8175
func testMultipleObserversAllReceiveCallbacks() {
@@ -94,7 +88,7 @@ final class NetworkObserverTests: XCTestCase {
9488
// Both observers should receive callbacks
9589
XCTAssertEqual(observer1.willSendCount, 1, "Observer 1 willSendRequest should be called")
9690
XCTAssertEqual(observer2.willSendCount, 1, "Observer 2 willSendRequest should be called")
97-
XCTAssertGreaterThanOrEqual(observer1.didReceiveCount + observer1.didFailCount, 1)
98-
XCTAssertGreaterThanOrEqual(observer2.didReceiveCount + observer2.didFailCount, 1)
91+
XCTAssertEqual(observer1.didReceiveCount, 1, "Observer 1 didReceiveResponse should be called")
92+
XCTAssertEqual(observer2.didReceiveCount, 1, "Observer 2 didReceiveResponse should be called")
9993
}
10094
}

0 commit comments

Comments
 (0)