Skip to content

Commit

Permalink
fix error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
maticzav committed May 2, 2023
1 parent 6cef4d1 commit 8d13e7d
Show file tree
Hide file tree
Showing 10 changed files with 88 additions and 50 deletions.
23 changes: 11 additions & 12 deletions Sources/SwiftGraphQLClient/Client/Operation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,44 +83,43 @@ public struct OperationResult: Equatable {
/// Data received from the server.
public var data: AnyCodable

/// Errors accumulated along the execution path.
public var errors: [CombinedError]
/// Execution error encontered in one of the exchanges in the chain.
///
/// When we use a GraphQL API there are two kinds of errors we may encounter: Network Errors and GraphQL Errors from the API. Since it's common to encounter either of them, there's a CombinedError class that can hold and abstract either.
public var error: ExecutionError?

/// Optional stale flag added by exchanges that return stale results.
public var stale: Bool?

public init(
operation: Operation,
data: AnyCodable,
errors: [CombinedError],
error: ExecutionError? = nil,
stale: Bool? = nil
) {
self.operation = operation
self.data = data
self.errors = errors
self.error = error
self.stale = stale
}
}


/// An error structure describing an error that may have happened in one of the exchanges.
public enum CombinedError: Error {
public enum ExecutionError: Error {

/// Describes an error that occured on the networking layer.
case network(URLError)

/// Describes errors that occured during the GraphQL execution.
case graphql([GraphQLError])

/// Describes an error that occured during the parsing phase on the client (e.g. received JSON is invalid).
case parsing(Error)

/// An error occured and it's not clear why.
case unknown(Error)
}

extension CombinedError: Equatable {
public static func == (lhs: CombinedError, rhs: CombinedError) -> Bool {
extension ExecutionError: Equatable {
public static func == (lhs: ExecutionError, rhs: ExecutionError) -> Bool {
switch (lhs, rhs) {
case let (.graphql(l), .graphql(r)):
return l == r
Expand Down Expand Up @@ -162,8 +161,8 @@ public struct DecodedOperationResult<T> {
/// Data received from the server.
public var data: T

/// Errors accumulated along the execution path.
public var errors: [CombinedError]
/// Execution error encountered in one of the exchanges.
public var error: ExecutionError?

/// Tells wether the result of the query is ot up-to-date.
public var stale: Bool?
Expand Down
55 changes: 44 additions & 11 deletions Sources/SwiftGraphQLClient/Client/Selection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,19 +119,52 @@ extension GraphQLClient {
extension OperationResult {

/// Decodes data in operation result using the selection decoder.
fileprivate func decode<T, TypeLock>(
selection: Selection<T, TypeLock>
) throws -> DecodedOperationResult<T> {
let data = try selection.decode(raw: self.data)
fileprivate func decode<T, TypeLock>(selection: Selection<T, TypeLock>) throws -> DecodedOperationResult<T> {
// NOTE: One of four things might happen as described in http://spec.graphql.org/October2021/#sec-Response-Format:
// 1. execution was successful: `data` field is present, `error` is not;
// 2. error was raised before the execution began: `error` field is present, `data` is not;
// 3. error was raised during exeuction: `error` field is present, `data` is nil;
// 4. field error occurred: both `data` and `error` fiels are present.
// Of the above four cases, we can be confident that the contract hasn't been broken in cases
// 1) and 4). In other cases, it's possible that even though the resolver expected a value it encountered
// an error and received `nil` instead. In such cases, we need to terminate the pipeline altogether.

switch (self.data.value) {
#if canImport(Foundation)
case is NSNull, is Void, Optional<AnyDecodable>.none:
if let error = self.error {
throw error
}
// NOTE: This should never happen if the server follows the GraphQL Specification!
throw OperationError.missingBothDataAndErrorFields
#else
case is Void, Optional<AnyDecodable>.none:
if let error = self.error {
throw error
}
throw OperationError.missingBothDataAndErrorFields
#endif

default:
let data = try selection.decode(raw: self.data)

let result = DecodedOperationResult(
operation: self.operation,
data: data,
error: self.error,
stale: self.stale
)

return result
}

let result = DecodedOperationResult(
operation: self.operation,
data: data,
errors: self.errors,
stale: self.stale
)

return result
}
}

public enum OperationError: Error {
case missingBothDataAndErrorFields
}

#endif

2 changes: 1 addition & 1 deletion Sources/SwiftGraphQLClient/Exchanges/CacheExchange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public class CacheExchange: Exchange {

// Cache query result and operation references.
// (AnyCodable represents nil values as Void objects.)
if result.operation.kind == .query, result.errors.isEmpty {
if result.operation.kind == .query, result.error == nil {
self.resultCache[result.operation.id] = result

// NOTE: cache-only operations never receive data from the
Expand Down
6 changes: 3 additions & 3 deletions Sources/SwiftGraphQLClient/Exchanges/ErrorExchange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import Foundation
public struct ErrorExchange: Exchange {

/// Callback function that the exchange calls for every error in the operation result.
private var onError: (CombinedError, Operation) -> Void
private var onError: (ExecutionError, Operation) -> Void

public init(onError: @escaping (CombinedError, Operation) -> Void) {
public init(onError: @escaping (ExecutionError, Operation) -> Void) {
self.onError = onError
}

Expand All @@ -20,7 +20,7 @@ public struct ErrorExchange: Exchange {
) -> AnyPublisher<OperationResult, Never> {
next(operations)
.handleEvents(receiveOutput: { result in
for error in result.errors {
if let error = result.error {
self.onError(error, result.operation)
}
})
Expand Down
13 changes: 9 additions & 4 deletions Sources/SwiftGraphQLClient/Exchanges/FetchExchange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,22 @@ public class FetchExchange: Exchange {
return OperationResult(
operation: operation,
data: result.data,
errors: [.graphql(errors)],
error: .graphql(errors),
stale: false
)
}

return OperationResult(operation: operation, data: result.data, errors: [], stale: false)
return OperationResult(
operation: operation,
data: result.data,
error: nil,
stale: false
)
} catch(let err) {
return OperationResult(
operation: operation,
data: AnyCodable(()),
errors: [.parsing(err)],
error: .unknown(err),
stale: false
)
}
Expand All @@ -105,7 +110,7 @@ public class FetchExchange: Exchange {
let result = OperationResult(
operation: operation,
data: nil,
errors: [.network(error)],
error: .network(error),
stale: false
)

Expand Down
4 changes: 2 additions & 2 deletions Sources/SwiftGraphQLClient/Exchanges/WebSocketExchange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,12 @@ public class WebSocketExchange: Exchange {
var op = OperationResult(
operation: operation,
data: exec.data,
errors: [],
error: nil,
stale: false
)

if let errors = exec.errors {
op.errors = [.graphql(errors)]
op.error = .graphql(errors)
}
return op
}
Expand Down
14 changes: 7 additions & 7 deletions Tests/SwiftGraphQLClientTests/Exchanges/CacheExchangeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ final class CacheExchangeTests: XCTestCase {
results.send(SwiftGraphQLClient.OperationResult(
operation: op,
data: AnyCodable("hello"),
errors: [],
error: nil,
stale: false
))
operations.send(op)
Expand All @@ -136,14 +136,14 @@ final class CacheExchangeTests: XCTestCase {
results.send(SwiftGraphQLClient.OperationResult(
operation: op,
data: AnyCodable("hello"),
errors: [],
error: nil,
stale: false
))
operations.send(op)
results.send(SwiftGraphQLClient.OperationResult(
operation: op,
data: AnyCodable("world"),
errors: [],
error: nil,
stale: false
))
}
Expand Down Expand Up @@ -175,7 +175,7 @@ final class CacheExchangeTests: XCTestCase {
results.send(SwiftGraphQLClient.OperationResult(
operation: op,
data: AnyCodable("hello"),
errors: [],
error: nil,
stale: false
))

Expand All @@ -201,7 +201,7 @@ final class CacheExchangeTests: XCTestCase {
results.send(SwiftGraphQLClient.OperationResult(
operation: op,
data: AnyCodable("hello"),
errors: [],
error: nil,
stale: false
))

Expand Down Expand Up @@ -243,15 +243,15 @@ final class CacheExchangeTests: XCTestCase {
results.send(SwiftGraphQLClient.OperationResult(
operation: op,
data: AnyCodable("hello"),
errors: [],
error: nil,
stale: false
))

// somehow receive mutation result
results.send(SwiftGraphQLClient.OperationResult(
operation: CacheExchangeTests.mutationOperation,
data: AnyCodable("much data"),
errors: [],
error: nil,
stale: false
))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ final class ComposeExchangeTests: XCTestCase {
SwiftGraphQLClient.OperationResult(
operation: operation,
data: nil,
errors: [],
error: nil,
stale: false
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ final class DedupExchangeTests: XCTestCase {
results.send(SwiftGraphQLClient.OperationResult(
operation: DedupExchangeTests.queryOperation,
data: nil,
errors: [],
error: nil,
stale: false
))
operations.send(DedupExchangeTests.queryOperation)
Expand Down
17 changes: 9 additions & 8 deletions Tests/SwiftGraphQLClientTests/Exchanges/FetchExchangeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ final class FetchExchangeTests: XCTestCase {
XCTAssertEqual(result, OperationResult(
operation: result.operation,
data: AnyCodable("hello"),
errors: [],
error: nil,
stale: false)
)

Expand Down Expand Up @@ -126,13 +126,14 @@ final class FetchExchangeTests: XCTestCase {
return downstream
}
.sink { result in
XCTAssertEqual(result, OperationResult(
operation: result.operation,
data: nil,
errors: [
.network(URLError(rawValue: 400))
],
stale: false)
XCTAssertEqual(
result,
OperationResult(
operation: result.operation,
data: nil,
error: .network(URLError(rawValue: 400)),
stale: false
)
)

expectation.fulfill()
Expand Down

0 comments on commit 8d13e7d

Please sign in to comment.