Errors & Rejections
GraphQL is a great tool for exposing APIs and like any solution, it has its biases. Error handling is a very good example.
In GraphQL all the errors, or rather non-nominal responses, go through the
errors
key. For example in GraphQL if you send an unauthenticated request to an API that requires authentication you'll get a response of this type:query MyQuery {
user {
id
}
}
{
"data":{
},
"errors":[
{
"extensions":{
"code":"UNAUTHENTICATED",
"exception":{
"stackstrace":[
"..."
]
}
}
}
]
}
This
errors
response is part of Graphql Specification and is described as a field containing a list of non-typed objects in the API contract (aka the Graphql schema). This implies that when you call a classic GraphQL API, the errors that occur are not guaranteed to be structured in a certain way - this is on purpose!GraphQL creators chose this loosely coupled design for the errors format, arguing that a tight coupling would be too fragile and would too easily break communication between clients and servers.
Swan, as a financial institution, must handle many business rules that guarantee proper functioning of its system. Each request for modification (aka mutation) sent to the API can be subjected to dozens of different business verifications and therefore have as many different possible responses.
Therefore, it seems important to us you be able to easily understand and react to these different possible responses in your code.
For this reason, we've introduced the
Rejection
concept to our API.A
Rejection
is a GraphQL type returned by mutations, and only mutations, from the Swan API when a request is rejected due to a business rule. Here is how a Rejection
is described in our schema:interface Rejection {
message: String!
}
That is, an object containing a descriptive message.
Each business rule that can provoke a
Rejection
will have a type extending the Rejection
interface. For example, if you try to access a Consent
that doesn't exist, you will receive a Rejection
in return, described like this:type ConsentNotFoundRejection implements Rejection {
message: String!
consentId: String!
}
In addition to the message, the id of the unfound
Consent
will be specified.A mutation at Swan can therefore either return the result "a success" or
Rejection
. The cancelConsent
mutation is a simple illustration.type Mutation {
cancelConsent(input: CancelConsentInput!): CancelConsentPayload!
}
union CancelConsentPayload = CancelConsentSuccessPayload | ConsentNotFoundRejection
type CancelConsentSuccessPayload {
consent: Consent!
}
If the Consent was canceled, the API will return a
CancelConsentPayload
containing the cancelled Consent
, if the consent was not found it will return ConsentNotFoundRejection
.Rejection
doesn't make errors
disappear from the API. In fact, Rejections are planned for in Swan's system, and correspond to requests that fail due to business rules. For all other errors, unexpected ones (like a request that doesn't correspond to the API contract, or an unavailable Swan service), the mutations and the queries will return errors
in classic GraphQL format.In the Swan Banking Frontend open source project, there is a script to extract every object that implements a
Rejection
. The script then compiles the list of objects in a locales
file with all rejection keys (rejection.{name}
).Now that the concept of
Rejection
is more clear (if not, read the section just above), let's look at how to make requests to Swan API and handle different cases. Let's take the example of ourcancelConsent
.Imagine you want to fetch consent, but you don't actually care about the different
Rejection
.You would make a request like this:
mutation MyMutation {
cancelConsent(input: {consentId: "{{YOUR_CONSENT_ID}}"}) {
... on CancelConsentSuccessPayload {
consent {
id
}
}
}
}
✅If it's a success you'll get this:
{
"data": {
"cancelConsent": {
"consent": {
"id" : "{{YOUR_CONSENT_ID}}"
}
}
}
}
🛑But if it's rejected, you'll have no explanation:
{
"data": {
"cancelConsent": {}
}
}
A more sensible approach would be to modify the request so you can obtain the reason for rejection. Just add:
... On Rejection
:mutation MyMutation {
cancelConsent(input: {consentId: "{{YOUR_CONSENT_ID}}"}) {
... on CancelConsentSuccessPayload {
consent {
id
}
}
... on Rejection {
message
}
}
}
✅If it's a success you'll get this:
{
"data": {
"cancelConsent": {
"consent": {
"id" : "{{YOUR_CONSENT_ID}}"
}
}
}
}
🛑But if it's rejected, you'll get this message:
{
"data": {
"cancelConsent": {
"message": "Consent with id '{{YOUR_ID}}' was not found."
}
}
}
Lastly if you wish to finely filter the different
Rejection
from the API you can add as many ... On ARejectionName
as the number of Rejection
returned:mutation MyMutation {
cancelConsent(input: {consentId: "{{YOUR_CONSENT_ID}}"}) {
... on CancelConsentSuccessPayload {
consent {
id
}
}
... on Rejection {
... on ConsentNotFoundRejection{
__typename
consentId
}
message
}
}
}
✅If it's a success you'll get this:
{
"data": {
"cancelConsent": {
"consent": {
"id" : "{{YOUR_CONSENT_ID}}"
}
}
}
}
🛑 But if it's rejected, you'll get the message, the type, and all the supplementary data:
{
"data": {
"cancelConsent": {
"__typename": "ConsentNotFoundRejection",
"consentId": "{{YOUR_CONSENT_ID}}",
"message": "Consent with id '{{YOUR_CONSENT_ID}}' was not found."
}
}
}
Last modified 2mo ago