Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error extensions support #96

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

itayAmza
Copy link

@itayAmza itayAmza commented Jan 5, 2022

Following GraphQLError extension to add error code #72 on Graphiti.
I was looking for a better way to handle GraphQL errors on client.
Inspired by Apollo Server error extensions I was looking for something similar for my Vapor server.

Swift Error protocol

Most of the times no additional data needed to analyze an error or a thrown exception but the data already exist in a Swift custom error type.
Vapor's Abort and AbortError for example already have status code, reason, stack trace and even headers.

Problem 1
Accessing custom error data without casting it to its actual type is impossible.

Problem 2
Even if I can access this data, saving this Any as a Codable representation will require using some king of meta type or type erasure.

Codable+Extensions - AnyCodable

Inspired by Flight-School/AnyCodable I've crated AnyCodable class, along side the existing AnyEncodable, that can wrap Any Codable type, and encode\decode it ad a dictionary value.

Error+Extensions

Using Swift's Mirror I was able to create a dictionary representation reflecting all of the error's additional data, that otherwise was hidden to the GraphQL module.
I've added a new Swift Error extension to easily create reflection for a given error.

public extension Error {
    var reflection: [String: AnyCodable] {
        let errorReflection: Mirror = Mirror(reflecting: self)
        return Dictionary(uniqueKeysWithValues: errorReflection.children.lazy.map({ (label: String?, value: Any) -> (String, AnyCodable)? in
            guard let key = label,
                  let codableValue = value as? Codable else {
                      return nil
            }
            
            return (key, AnyCodable(codableValue))
        }).compactMap { $0 })
    }
}

GraphQLError

Next step was to add extensions property to the GraphQLError struct.

    /**
     * A dictionary containing original error reflection
     * to supply additional data from error extensions.
     *
     * Will reflect only Codable objects.
     *
     * For more information about supported types look at `AnyCodable` - `init(from:)`
     *
     * Appears in the result of `description`.
     */
    public let extensions: [String: AnyCodable]?

The result

Now error description response contains a new extensions field that reflect any additional data held by the original error.

{
    "errors": [
        {
            "path": [
                “getMe
            ],
            "message": "Abort.401: Unauthorized",
            "extensions": {
                "status": 401,
                "reason": "Unauthorized",
                "identifier": “401”,
                "headers": {
                    “header1": “some header”,
                    “header2”: "some other header”
                }
            },
            "locations": [
                {
                    "line": 2,
                    "column": 3
                }
            ]
        }
    ]
}

TODOs and other considerations

  • The AnyCodable class does not represent nil values as for now, any nil decoded value will be presented as "Null" string
  • AnyCodable supported types can be found on the init(from:) initializer. any new type that we want to support in the future will have to be added manually.
  • I can't seems to find an easy way to add a kill switch to this feature, in case some user will decide he does not want to reveal this data to the client.

@d-exclaimation
Copy link
Member

Sorry for butting in. I am just wondering whether the GraphQLError extensions field and the Error reflection could have used the existing Map enum instead of AnyCodable. I am quite unfamiliar with AnyCodable from Flight-School/AnyCodable, so I might have missed something here.

@itayAmza
Copy link
Author

Sorry for butting in. I am just wondering whether the GraphQLError extensions field and the Error reflection could have used the existing Map enum instead of AnyCodable. I am quite unfamiliar with AnyCodable from Flight-School/AnyCodable, so I might have missed something here.

It's good man, any feedback is welcome. map might work here, but even better swift 5.7 can make things easier with the built in any type erasure.

@NeedleInAJayStack
Copy link
Member

Hey guys, sorry I'm kinda late to this party. I forgot this MR existed and made a new MR adding this functionality using the GraphQL Map object like d-exclaimation suggested. @itayAmza could you see if it meets your needs as well?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants