Skip to content

Commit 0c56d45

Browse files
Merge pull request #770 from kimdv/kimdv/add-retry-if-graphql-errors
Introducing delegate if there is GraphQL errors
2 parents d45f01b + 2e8d94a commit 0c56d45

3 files changed

Lines changed: 163 additions & 12 deletions

File tree

Sources/Apollo/HTTPNetworkTransport.swift

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,21 @@ public protocol HTTPNetworkTransportRetryDelegate: HTTPNetworkTransportDelegate
6565
retryHandler: @escaping (_ shouldRetry: Bool) -> Void)
6666
}
6767

68+
/// Methods which will be called after some kind of response has been received and it contains GraphQLErrors
69+
public protocol HTTPNetworkTransportGraphQLErrorDelegate: HTTPNetworkTransportDelegate {
70+
71+
72+
/// Called when response contains one or more GraphQL errors.
73+
/// NOTE: Don't just call the `retryHandler` with `true` all the time, or you can potentially wind up in an infinite loop of errors
74+
///
75+
/// - Parameters:
76+
/// - networkTransport: The network transport which received the error
77+
/// - errors: The received GraphQL errors
78+
/// - retryHandler: A closure indicating whether the operation should be retried. Asyncrhonous to allow for re-authentication or other async operations to complete.
79+
func networkTransport(_ networkTransport: HTTPNetworkTransport, receivedGraphQLErrors errors: [GraphQLError], retryHandler: @escaping (_ shouldRetry: Bool) -> Void)
80+
}
81+
82+
6883
// MARK: -
6984

7085
/// A network transport that uses HTTP POST requests to send GraphQL operations to a server, and that uses `URLSession` as the networking implementation.
@@ -131,6 +146,7 @@ public class HTTPNetworkTransport {
131146

132147
if let receivedError = error {
133148
self.handleErrorOrRetry(operation: operation,
149+
files: files,
134150
error: receivedError,
135151
for: request,
136152
response: response,
@@ -147,6 +163,7 @@ public class HTTPNetworkTransport {
147163
response: httpResponse,
148164
kind: .errorResponse)
149165
self.handleErrorOrRetry(operation: operation,
166+
files: files,
150167
error: unsuccessfulError,
151168
for: request,
152169
response: response,
@@ -159,6 +176,7 @@ public class HTTPNetworkTransport {
159176
response: httpResponse,
160177
kind: .invalidResponse)
161178
self.handleErrorOrRetry(operation: operation,
179+
files: files,
162180
error: error,
163181
for: request,
164182
response: response,
@@ -170,10 +188,13 @@ public class HTTPNetworkTransport {
170188
guard let body = try self.serializationFormat.deserialize(data: data) as? JSONObject else {
171189
throw GraphQLHTTPResponseError(body: data, response: httpResponse, kind: .invalidResponse)
172190
}
191+
173192
let graphQLResponse = GraphQLResponse(operation: operation, body: body)
193+
174194
if let errors = graphQLResponse.parseErrorsOnlyFast() {
175195
// Handle specific errors from response
176196
self.handleGraphQLErrorsIfNeeded(operation: operation,
197+
files: files,
177198
for: request,
178199
body: body,
179200
errors: errors,
@@ -183,6 +204,7 @@ public class HTTPNetworkTransport {
183204
}
184205
} catch let parsingError {
185206
self.handleErrorOrRetry(operation: operation,
207+
files: files,
186208
error: parsingError,
187209
for: request,
188210
response: response,
@@ -194,29 +216,60 @@ public class HTTPNetworkTransport {
194216

195217
return task
196218
}
219+
220+
private func handleGraphQLErrorsOrComplete<Operation>(operation: Operation,
221+
files: [GraphQLFile]?,
222+
response: GraphQLResponse<Operation>,
223+
completionHandler: @escaping (_ result: Result<GraphQLResponse<Operation>, Error>) -> Void) {
224+
guard let delegate = self.delegate as? HTTPNetworkTransportGraphQLErrorDelegate,
225+
let graphQLErrors = response.parseErrorsOnlyFast(),
226+
!graphQLErrors.isEmpty else {
227+
completionHandler(.success(response))
228+
return
229+
}
230+
231+
delegate.networkTransport(self, receivedGraphQLErrors: graphQLErrors, retryHandler: { [weak self] shouldRetry in
232+
guard let self = self else {
233+
// None of the rest of this really matters
234+
return
235+
}
236+
237+
guard shouldRetry else {
238+
completionHandler(.success(response))
239+
return
240+
}
241+
242+
_ = self.send(operation: operation,
243+
isPersistedQueryRetry: self.enableAutoPersistedQueries,
244+
files: files,
245+
completionHandler: completionHandler)
246+
})
247+
}
197248

198249
private func handleGraphQLErrorsIfNeeded<Operation>(operation: Operation,
250+
files: [GraphQLFile]?,
199251
for request: URLRequest,
200252
body: JSONObject,
201253
errors: [GraphQLError],
202254
completionHandler: @escaping (_ results: Result<GraphQLResponse<Operation>, Error>) -> Void) {
203-
255+
204256
let errorMessages = errors.compactMap { $0.message }
205257
if self.enableAutoPersistedQueries,
206258
errorMessages.contains("PersistedQueryNotFound") {
207-
// We need to retry this with the full body.
208-
_ = self.send(operation: operation,
209-
isPersistedQueryRetry: true,
210-
files: nil,
211-
completionHandler: completionHandler)
259+
// We need to retry this with the full body.
260+
_ = self.send(operation: operation,
261+
isPersistedQueryRetry: true,
262+
files: nil,
263+
completionHandler: completionHandler)
212264
} else {
213265
// Pass the response on to the rest of the chain
214266
let response = GraphQLResponse(operation: operation, body: body)
215-
completionHandler(.success(response))
267+
handleGraphQLErrorsOrComplete(operation: operation, files: files, response: response, completionHandler: completionHandler)
216268
}
217269
}
218-
270+
219271
private func handleErrorOrRetry<Operation>(operation: Operation,
272+
files: [GraphQLFile]?,
220273
error: Error,
221274
for request: URLRequest,
222275
response: URLResponse?,
@@ -234,12 +287,20 @@ public class HTTPNetworkTransport {
234287
for: request,
235288
response: response,
236289
retryHandler: { [weak self] shouldRetry in
290+
guard let self = self else {
291+
// None of the rest of this really matters
292+
return
293+
}
294+
237295
guard shouldRetry else {
238296
completionHandler(.failure(error))
239297
return
240298
}
241299

242-
_ = self?.send(operation: operation, completionHandler: completionHandler)
300+
_ = self.send(operation: operation,
301+
isPersistedQueryRetry: self.enableAutoPersistedQueries,
302+
files: files,
303+
completionHandler: completionHandler)
243304
})
244305
}
245306

@@ -287,7 +348,7 @@ public class HTTPNetworkTransport {
287348
sendQueryDocument: sendQueryDocument,
288349
autoPersistQueries: autoPersistQueries)
289350
}
290-
351+
291352
private func createRequest<Operation: GraphQLOperation>(for operation: Operation,
292353
files: [GraphQLFile]?,
293354
httpMethod: GraphQLHTTPMethod,

Tests/ApolloTestSupport/MockURLSession.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,27 @@ import Foundation
99

1010
public final class MockURLSession: URLSession {
1111
public private (set) var lastRequest: URLRequest?
12-
12+
13+
public var data: Data?
14+
public var response: HTTPURLResponse?
15+
public var error: Error?
16+
1317
override public func dataTask(with request: URLRequest) -> URLSessionDataTask {
1418
lastRequest = request
1519
return URLSessionDataTaskMock()
1620
}
1721

1822
override public func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
1923
lastRequest = request
24+
if let response = response {
25+
completionHandler(data, response, error)
26+
}
2027
return URLSessionDataTaskMock()
2128
}
2229
}
2330

2431
private final class URLSessionDataTaskMock: URLSessionDataTask {
2532
override func resume() {
33+
2634
}
2735
}

Tests/ApolloTests/HTTPTransportTests.swift

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import XCTest
1010
@testable import Apollo
1111
import StarWarsAPI
12+
import ApolloTestSupport
1213

1314
class HTTPTransportTests: XCTestCase {
1415

@@ -23,6 +24,8 @@ class HTTPTransportTests: XCTestCase {
2324
private var shouldModifyURLInWillSend = false
2425
private var retryCount = 0
2526

27+
private var graphQlErrors = [GraphQLError]()
28+
2629
private lazy var url = URL(string: "http://localhost:8080/graphql")!
2730
private lazy var networkTransport = HTTPNetworkTransport(url: self.url,
2831
useGETForQueries: true,
@@ -59,7 +62,7 @@ class HTTPTransportTests: XCTestCase {
5962
line: line)
6063
}
6164
}
62-
65+
6366
func testPreflightDelegateTellingRequestNotToSend() {
6467
self.shouldSend = false
6568

@@ -193,6 +196,74 @@ class HTTPTransportTests: XCTestCase {
193196
delegate: self)
194197
XCTAssertNotEqual(self.networkTransport, nonIdenticalTransport)
195198
}
199+
200+
func testErrorDelegateWithErrors() throws {
201+
self.retryCount = 0
202+
self.graphQlErrors = []
203+
let query = HeroNameQuery()
204+
// TODO: Replace this with once it is codable https://github.com/apollographql/apollo-ios/issues/467
205+
let body = ["errors": [["message": "Test graphql error"]]]
206+
207+
let mockSession = MockURLSession()
208+
mockSession.response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)
209+
mockSession.data = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted)
210+
let network = HTTPNetworkTransport(url: url, session: mockSession, delegate: self)
211+
let expectation = self.expectation(description: "Send operation completed")
212+
213+
let _ = network.send(operation: query) { result in
214+
switch result {
215+
case .success:
216+
expectation.fulfill()
217+
case .failure:
218+
break
219+
}
220+
}
221+
222+
guard let request = mockSession.lastRequest else {
223+
XCTFail("last request should not be nil")
224+
return
225+
}
226+
XCTAssertEqual(request.url?.host, network.url.host)
227+
XCTAssertEqual(request.httpMethod, "POST")
228+
229+
XCTAssertEqual(self.graphQlErrors.count, 1)
230+
XCTAssertEqual(retryCount, 1)
231+
wait(for: [expectation], timeout: 1)
232+
}
233+
234+
func testErrorDelegateWithNoErrors() throws {
235+
self.retryCount = 0
236+
self.graphQlErrors = []
237+
let query = HeroNameQuery()
238+
// TODO: Replace this with once it is codable https://github.com/apollographql/apollo-ios/issues/467
239+
let body = ["errors": []]
240+
241+
let mockSession = MockURLSession()
242+
mockSession.response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)
243+
mockSession.data = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted)
244+
let network = HTTPNetworkTransport(url: url, session: mockSession, delegate: self)
245+
let expectation = self.expectation(description: "Send operation completed")
246+
247+
let _ = network.send(operation: query) { result in
248+
switch result {
249+
case .success:
250+
expectation.fulfill()
251+
case .failure:
252+
break
253+
}
254+
}
255+
256+
guard let request = mockSession.lastRequest else {
257+
XCTFail("last request should not be nil")
258+
return
259+
}
260+
XCTAssertEqual(request.url?.host, network.url.host)
261+
XCTAssertEqual(request.httpMethod, "POST")
262+
XCTAssertEqual(self.retryCount, 0)
263+
XCTAssertEqual(self.graphQlErrors.count, 0)
264+
wait(for: [expectation], timeout: 1)
265+
266+
}
196267
}
197268

198269
// MARK: - HTTPNetworkTransportPreflightDelegate
@@ -266,3 +337,14 @@ extension HTTPTransportTests: HTTPNetworkTransportRetryDelegate {
266337
}
267338
}
268339
}
340+
341+
// MARK: - HTTPNetworkTransportGraphQLErrorDelegate
342+
343+
extension HTTPTransportTests: HTTPNetworkTransportGraphQLErrorDelegate {
344+
func networkTransport(_ networkTransport: HTTPNetworkTransport, receivedGraphQLErrors errors: [GraphQLError], retryHandler: @escaping (Bool) -> Void) {
345+
self.retryCount += 1
346+
let shouldRetry = retryCount == 2
347+
self.graphQlErrors = errors
348+
retryHandler(shouldRetry)
349+
}
350+
}

0 commit comments

Comments
 (0)