DocumentationAPIError Handling

Error Handling

By default, Sofa returns a response that includes JSON representation of thrown error object from GraphQL with HTTP status code 500. But, you can enhance error handler by adding your errorHandler function.

api.use(
  '/api',
  useSofa({
    schema,
    // `errors` is the array containing the `Error` objects
    errorHandler(errors) {
      for (const error of errors) {
        console.error(`Error: ${error.message}`);
      }
      return new Response(errs[0].message, {
        status: 500,
        headers: { 'Content-Type': 'application/json' },
      });
    },
  })
);

By default, it always returns a response with 200 if the request is valid. If the request is invalid, it returns a response with 400 status code and the error message.

const res = await fetch('http://localhost:4000/api/createUser', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 1, // Invalid name
  }),
});
 
console.log(res.status); // 400
const data = await res.json();
console.log(data); // {"errors":[{"message":"Expected type String, found 1."}]}

HTTP Error Extensions

Just like GraphQL Yoga’s error handling , SOFA respects the status code and headers provided in the error extensions.

GraphQL Error with http extensions.
throw new GraphQLError(
  `User with id '${args.byId}' not found.`,
  // error extensions
  {
    extensions: {
      http: {
        status: 400,
        headers: {
          'x-custom-header': 'some-value',
        },
      },
    },
  }
);

In this case, you returns a response with 400 status code and x-custom-header in the response headers.

Let’s say you have a simple GraphQL API like below;

import { createServer } from 'node:http';
import { useSofa } from 'sofa-api';
import { makeExecutableSchema } from '@graphql-tools/schema';
 
createServer(
  useSofa({
    basePath: '/api',
    schema: makeExecutableSchema({
      typeDefs: /* GraphQL */ `
        type Query {
          posts: [Post!]!
        }
        type Post {
          id: ID!
          title: String!
          secret: String!
        }
      `,
      resolvers: {
        Query: {
          posts() {
            return getPosts();
          },
        },
        Post: {
          async secret(_, __, { request }) {
            const authHeader = request.headers.get('Authorization');
            if (!authHeader) {
              throw new GraphQLError('Unauthorized', {
                extensions: {
                  http: {
                    status: 401,
                    headers: {
                      'WWW-Authenticate': 'Bearer',
                    },
                  },
                },
              });
            }
            const [type, token] = authHeader.split(' ');
            if (type !== 'Bearer') {
              throw new GraphQLError('Invalid token type', {
                extensions: {
                  http: {
                    status: 401,
                    headers: {
                      'WWW-Authenticate': 'Bearer',
                    },
                  },
                },
              });
            }
            if (token !== 'secret') {
              throw new GraphQLError('Invalid token', {
                extensions: {
                  http: {
                    status: 401,
                    headers: {
                      'WWW-Authenticate': 'Bearer',
                    },
                  },
                },
              });
            }
            return 'Secret value';
          },
        },
      },
    }),
  })
).listen(4000);

In this case if you make a request to /api/posts without a valid Authorization header, you will get a response with 401 status code and WWW-Authenticate in the response headers. But the response body will contain the data and errors.

const res = await fetch('http://localhost:4000/api/posts');
console.log(res.status); // 401
console.log(res.headers.get('WWW-Authenticate')); // Bearer
const data = await res.json();
expect(data).toEqual({
  data: {
    posts: [
      {
        id: '1',
        title: 'Post 1',
        secret: null,
      },
      {
        id: '2',
        title: 'Post 2',
        secret: null,
      },
    ],
  },
  errors: [
    {
      message: 'Unauthorized',
      path: ['posts', 'secret'],
    },
  ],
});

In this case only errored fields will be null in the response body.

However if you make a request to /api/me with x-user-id header, you will get a response with 200 status code and x-custom-header in the response headers.

const res = await fetch('http://localhost:4000/api/posts', {
  headers: {
    Authorization: 'Bearer secret',
  },
});
console.log(res.status); // 200
const data = await res.json();
expect(data).toEqual({
  data: {
    posts: [
      {
        id: '1',
        title: 'Post 1',
        secret: 'Secret value',
      },
      {
        id: '2',
        title: 'Post 2',
        secret: 'Secret value',
      },
    ],
  },
});