Links

Errors & Rejections

Errors

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 errorsresponse 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.

Rejections

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 Consentwill 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.

Extract all possible rejections

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}).

How do I use Rejections ?

Now that the concept ofRejection 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."
}
}
}