Graphql error internal server error

GraphQL на русском — здесь много статей и видео об этой замечательной технологии.

conf-talks

В любом приложении возникают ошибки, и в вашим GraphQL API они тоже будут. Се ля ви.

Как работать с ошибками в GraphQL? К чему необходимо быть готовым клиентам вашего АПИ? Как лучше возвращать ошибки клиенту? Да и как вообще они возвращаются в GraphQL? В этот статье мы разберем как работать с ошибками в GraphQL.

Для начала давайте бегло посмотрим какие ошибки могут возникать и сразу разобьем их на группы:

  • ФАТАЛЬНЫЕ ОШИБКИ
    • 500 Internal Server Error
    • кончилась память
    • забыли установить пакет
    • грубая синтаксическая ошибка в коде
  • ОШИБКИ ВАЛИДАЦИИ
    • ошибка невалидного GraphQL-запроса
    • запросили несуществующее поле
    • не передали обязательный аргумент
    • не передали переменную
  • RUNTIME ОШИБКИ В RESOLVE-МЕТОДАХ
    • throw new Error(“”)
    • undefined is not a function (юзайте Flowtype или TypeScript уже в конце концов)
    • ошибка невалидного значения в return
  • ПОЛЬЗОВАТЕЛЬСКИЕ ОШИБКИ
    • запись не найдена
    • недостаточно прав для просмотра или редактирования записи

Как обычно GraphQL-сервер отвечает на ошибки?

Если произошла фатальная ошибка, то сервер возвращает 500 код. Это как обычно.

Но вот что необычное в GraphQL, так если произошла любая другая ошибка сервер возвращает код 200. Обычно бывалые REST API разработчики на этом моменте хотят выпрыгнуть из окна. Никаких вам 401, 403, 404 и прочих кодов не будет.

Сделали это так, потому что GraphQL по спецификации не привязан ни к какому протоколу. Вы можете гонять GraphQL-запросы через websockets, ssh, telnet ну и обычный http. Коль нет жесткой привязки к протоколу, то ошибки все унесли в тело ответа.

Вот так выглядит ответ от GraphQL по спецификации:

{
  data: {}, // для возврата данных
  errors: [...], // для возврата ошибок, массив между прочим 😳
  extensions: {}, // объект для пользовательских данных, сюда пихайте что хотите
  // другие ключи запрещены по спеке!
}

Первое что бросается в глаза так это то, что GraphQL возвращает массив ошибок. Wow! Т.к. запрос может быть сложный с запросом кучи ресурсов, то GraphQL может вернуть вам часть данных, а на оставшуюся часть вернуть ошибки. И это хорошо, пол ответа лучше, чем ничего.

Фатальные ошибки

Фатальная ошибка чаще всего имеет следующий вид — 500 Internal Server Error. Возникает обычно если кончилась память, забыли установить пакет, совершили грубую синтаксическую ошибку в коде. Да много еще чего. При этом дело не доходит до обработки GraphQL-запроса. И здесь резонно вернуть 500 ошибку.

Нет работы GraphQL, нет кода 200.

Фронтендеры обычно это дело должны обрабатывать на уровне своего Network Layer’a. Получили 500, значит где-то косячнулись бэкендеры с админами.

Ошибки валидации

Сервер получил запрос и делегировал его в пакет graphql. Перед тем как GraphQL-запрос будет выполняться он проходит парсинг и валидацию. Если кривой запрос, то никакие resolve-методы вызваны не будут и тупо будет возвращена ошибка:

{
  errors: [
    {
      message: 'Cannot query field "wrong" on type "Query".',
      locations: [{ line: 3, column: 11 }],
    },
  ],
}

// или например такая
{
  errors: [
    {
      message: 'Variable "$q" of required type "String!" was not provided.',
      locations: [{ line: 2, column: 16 }],
    },
  ],
}

При этом сервер вернет статус 200. При коде 200, ошибка обычно на стороне фронтендера. Но и бекендер может быть к этому причастен, если взял и удалил из схемы какое-то поле. В таком случае все старые работающие приложения теперь стали отправлять невалидные запросы.

Runtime ошибки в resolve-методах

Если запрос прошел парсинг и валидацию, то он начинает выполняться и вызывать resolve-методы вашей схемы согласно присланному GraphQL-запросу. И если вдруг внутри resolve-метода вываливается Exception (throw new Error()), неважно явно вы его выбросили, или он прилетел из недр чужих пакетов. То происходит следующая магия:

  • обработка ветки графа приостанавливается (вложенные resolve-методы вызываться не будут)
  • на месте элемента, где произошла ошибка возвращается null
  • ошибка добавляется в массив errors
  • НО при этом соседние ветки продолжают работать

Хорошо это понять можно на примере следующего кода:

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: {
      search: {
        args: {
          q: { type: GraphQLString },
        },
        resolve: (_, args) => {
          if (!args.q) throw new Error('missing q');
          return { text: args.q };
        },
        type: new GraphQLObjectType({
          name: 'Record',
          fields: {
            text: {
              type: GraphQLString,
              resolve: source => source.text,
            },
          },
        }),
      },
    },
  }),
});

const res = await graphql({
  schema,
  source: `
    query {
      s1: search(q: "ok") { text }
      s2: search { text }
      s3: search(q: "good") { text }
    }
  `,
});

Ответ от сервера будет получен следующий:

{
  errors: [
    { message: 'missing q', locations: [{ line: 4, column: 11 }], path: ['s2'] }
  ],
  data: { s1: { text: 'ok' }, s2: null, s3: { text: 'good' } },
}

Поле s1 возвращает полный результат. В s2 была выброшена ошибка, поэтому оно стало null и в массив errors добавилась ошибка. И дальше поле s3 тоже без проблем вернулось.

Т.е. получается на тех местах, где была выброшена ошибка возвращается null и пишется ошибка в массив. А вся остальная часть запроса продолжает выполняться как ни в чем не бывало. Вот такой вот он добрый GraphQL, хоть что-нибудь да вернет.

Точно также работает, если бэкендер вернул данные неправильного типа в resolve-методе. GraphQL не позволяет вернуть “левые данные” в data.

Вот пример, когда мы по схеме должны вернуть массив строк, но второй элемент не является строкой. Вместо “левого” значения, он вернет null и при этом добавит ошибку в массив:

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: {
      ooops: {
        type: new GraphQLList(GraphQLString),
        resolve: () => ['ok', { hey: 'wrong non String value' }],
      },
    },
  }),
});

const res = await graphql(schema, `query { ooops }`);

expect(res).toEqual({
  errors: [
    {
      message: 'String cannot represent value: { hey: "wrong non String value" }',
      locations: [{ line: 3, column: 11 }],
      path: ['ooops', 1],
    },
  ],
  data: { ooops: ['ok', null] },
});

Также спецификация GraphQL позволяет передать дополнительные данные вместе с ошибкой через проперти extensions. Давайте создадим объект ошибки и присвоим ему два проперти extensions и someOtherData:

new GraphQLObjectType({
  name: 'Query',
  fields: {
    search: {
      resolve: () => {
        const e: any = new Error('Some error');
        e.extensions = { a: 1, b: 2 }; // will be passed in GraphQL-response
        e.someOtherData = { c: 3, d: 4 }; // will be omitted
        throw e;
      },
      type: GraphQLString,
    },
  },
});

На выходе в GraphQL-ответе мы получим следующие данные (extensions будет передан, а все другие проперти из объекта ошибки будут опущены, например не будет someOtherData из нашего примера):

{
  errors: [
    {
      message: 'Some error',
      locations: [{ line: 1, column: 9 }],
      path: ['search'],
      extensions: { a: 1, b: 2 },
    },
  ],
  data: { search: null },
}

Такой механизм позволяет передать клиентам дополнительные данные об ошибке.

Ну коль заговорили про фронтенд, давайте пофантазируем как им работать с такими ошибками. На верхнем уровне одну ошибку в модальном окне вывести не проблема, а если ошибок две? А если у нас сложное приложение и ошибки надо показывать в разных частях приложения? Вот тут у фронтендера начинается просто адская боль и печаль с таким массивом ошибок. Его надо отдельно парсить, понимать какая именно ошибка произошла (например через extensions.code). Как-то передать ошибку в нужную компоненту и на нужный уровень. В общем, приходится сильно изгаляться в коде пробросом лишних проперти и логикой.

Если вам интересно как бэкендер может упростить жизнь фронтендеру, то обязательно читайте следующий раздел.

Пользовательские ошибки

Что такое пользовательские ошибки? Ну это когда вам где-то в приложении надо вывести “запись не найдена”, или “у вас нет прав просматривать этот контент”, или “необходимо подтвердить возраст” или в списке на 23 элементе показать что “запись удалена”.

Если пользоваться стандартным механизмом ошибок GraphQL. То на фронтенде приходится сильно изгаляться, чтобы пробросить ошибку в нужное место.

Но эту проблему можно достаточно элегантно решить, если ошибки возвращать прямо в data на нужном уровне, а не через глобальный массив errors. Для этого в GraphQL есть Union-типы, которые возвращают либо запись с данными, либо ошибку.

Давайте сразу к живому примеру. Представим что нам надо вернуть список видео. Причем какие-то видео в обработке, другие перед просмотром необходимо купить или подтвердить свой возраст. Так давайте и будем возвращать список, который может вернуть Union-тип из Video, VideoInProgressProblem, VideoNeedBuyProblem и VideoApproveAgeProblem. Со стороны фронтендера можно тогда написать вот такой запрос:

query {
  list {
    __typename # <----- магическое поле, которое вернет имя типа для каждой записи
    ...on Video {
      title
      url
    }
    ...on VideoInProgressProblem {
      estimatedTime
    }
    ...on VideoNeedBuyProblem {
      price
    }
    ...on VideoApproveAgeProblem {
      minAge
    }
  }
}

Т.е. используем фрагменты на конкретных типах и запрашиваем поле __typename, которое возвращает имя типа. К запросу выше GraphQL-ответ будет следующий:

{
  data: {
    list: [
      { __typename: 'Video', title: 'DOM2 in the HELL', url: 'https://url' },
      { __typename: 'VideoApproveAgeProblem', minAge: 21 },
      { __typename: 'VideoNeedBuyProblem', price: 10 },
      { __typename: 'VideoInProgressProblem', estimatedTime: 220 },
    ],
  },
}

При таком подходе фронтендер знает какие вообще ошибки могут быть. Также он получает ошибки в нужной компоненте, на нужном уровне. Код захламляется только там, где необходимо разобрать разные варианты пользовательских ошибок и вывести либо данные, либо красивый блок с ошибочкой.

Причем фронтендеры могут легко понять, какой тип ошибки вернулся. И при этом получить дополнительные данные по ошибке, если она их возвращает. Это же просто обычный тип в схеме, который может содержать в себе любые необходимые поля.

Для себя я вынес одно правило, что пользовательским ошибкам лучше всего давать суффикс Problem, а не Error. Это позволяет избежать путаницы как на бэкенде, так и на фронтенде.

Как это дело можно организовать на бэкенде? Достаточно просто. Вот пример:

// Объявляем класс Видео
class Video {
  title: string;
  url: string;

  constructor({ title, url }) {
    this.title = title;
    this.url = url;
  }
}

// И сразу же объявим GraphQL-тип
const VideoType = new GraphQLObjectType({
  name: 'Video',
  fields: () => ({
    title: { type: GraphQLString },
    url: { type: GraphQLString },
  }),
});


// Объявим классы проблем (ошибок)
class VideoInProgressProblem {
  constructor({ estimatedTime }) {
    this.estimatedTime = estimatedTime;
  }
}
class VideoNeedBuyProblem {
  constructor({ price }) {
    this.price = price;
  }
}
class VideoApproveAgeProblem {
  constructor({ minAge }) {
    this.minAge = minAge;
  }
}

// И их типы для GraphQL
const VideoInProgressProblemType = new GraphQLObjectType({
  name: 'VideoInProgressProblem',
  fields: () => ({
    estimatedTime: { type: GraphQLInt },
  }),
});
const VideoNeedBuyProblemType = new GraphQLObjectType({
  name: 'VideoNeedBuyProblem',
  fields: () => ({
    price: { type: GraphQLInt },
  }),
});
const VideoApproveAgeProblemType = new GraphQLObjectType({
  name: 'VideoApproveAgeProblem',
  fields: () => ({
    minAge: { type: GraphQLInt },
  }),
});

// Ну а теперь самое интересное.
// Объявляем наш UNION-тип который будет возвращать либо видео, либо проблему-ошибку
const VideoResultType = new GraphQLUnionType({
  // Даем имя типу.
  // Здорово если если вы выработаете конвенцию в своей команде
  // и к таким Union-типам будете добавлять суффикс Result
  name: 'VideoResult',

  // как хорошие бекендеры добавляем какое-нибудь описание
  description: 'Video or problems',

  // объявляем типы через массив, которые могут быть возвращены
  types: () => [
    VideoType,
    VideoInProgressProblemType,
    VideoNeedBuyProblemType,
    VideoApproveAgeProblemType,
  ],

  // Ну и самое главное надо объявить функцию определения типа.
  // resolve-функции (смотри ниже поле Query.list) просто возвращают JS-объект
  // но вот GraphQL'ю нужно как-то JS-объект, сконвертировать в GraphQL-тип
  // иначе как он узнает что надо записать в поле __typename
  resolveType: value => {
    if (value instanceof Video) {
      return VideoType;
    } else if (value instanceof VideoInProgressProblem) {
      return VideoInProgressProblemType;
    } else if (value instanceof VideoNeedBuyProblem) {
      return VideoNeedBuyProblemType;
    } else if (value instanceof VideoApproveAgeProblem) {
      return VideoApproveAgeProblemType;
    }
    return null;
  },
});

// Ну и вишенка на торте
// Пишем простую схемку, которая нам возвращает массив из Видео и Ошибок-Проблем.
const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: {
      list: {
        type: new GraphQLList(VideoResultType),
        resolve: () => {
          return [
            new Video({ title: 'DOM2 in the HELL', url: 'https://url' }),
            new VideoApproveAgeProblem({ minAge: 21 }),
            new VideoNeedBuyProblem({ price: 10 }),
            new VideoInProgressProblem({ estimatedTime: 220 }),
          ];
        },
      },
    },
  }),
});

Очень просто и красиво. А самое главное удобно для фронтендеров:

  • знают какие ошибки могут быть
  • знают какие поля содержатся в ошибках
  • отлично поддерживается статический анализ, в отличии от обычных ошибок
  • ошибки возвращаются в дереве ответа, а не в глобальном массиве
  • в результате чище, проще и безопаснее код

Любите брата фронтендера своего 😉 Иначе они придут с вилами!

Ссылки по теме

  • Примеры кода в виде тестов к этой статье
  • Видео про ошибки от Sasha Solomon
  • Похожее видео про ошибки от Eloy Durán, всё-таки у Саши лучше

Как работать с ошибками в GraphQL?

В любом приложении возникают ошибки, и в вашим GraphQL API они тоже будут. Се ля ви.

Как работать с ошибками в GraphQL? К чему необходимо быть готовым клиентам вашего АПИ? Как лучше возвращать ошибки клиенту? Да и как вообще они возвращаются в GraphQL? В этот статье мы разберем как работать с ошибками в GraphQL.

Для начала давайте бегло посмотрим какие ошибки могут возникать и сразу разобьем их на группы:

  • ФАТАЛЬНЫЕ ОШИБКИ
    • 500 Internal Server Error
    • кончилась память
    • забыли установить пакет
    • грубая синтаксическая ошибка в коде
  • ОШИБКИ ВАЛИДАЦИИ
    • ошибка невалидного GraphQL-запроса
    • запросили несуществующее поле
    • не передали обязательный аргумент
    • не передали переменную
  • RUNTIME ОШИБКИ В RESOLVE-МЕТОДАХ
    • throw new Error(«»)
    • undefined is not a function (юзайте Flowtype или TypeScript уже в конце концов)
    • ошибка невалидного значения в return
  • ПОЛЬЗОВАТЕЛЬСКИЕ ОШИБКИ
    • запись не найдена
    • недостаточно прав для просмотра или редактирования записи

Как обычно GraphQL-сервер отвечает на ошибки?

Если произошла фатальная ошибка, то сервер возвращает 500 код. Это как обычно.

Но вот что необычное в GraphQL, так если произошла любая другая ошибка сервер возвращает код 200. Обычно бывалые REST API разработчики на этом моменте хотят выпрыгнуть из окна. Никаких вам 401, 403, 404 и прочих кодов не будет.

Сделали это так, потому что GraphQL по спецификации не привязан ни к какому протоколу. Вы можете гонять GraphQL-запросы через websockets, ssh, telnet ну и обычный http. Коль нет жесткой привязки к протоколу, то ошибки все унесли в тело ответа.

Вот так выглядит ответ от GraphQL по спецификации:

{
  data: {}, // для возврата данных
  errors: [...], // для возврата ошибок, массив между прочим 😳
  extensions: {}, // объект для пользовательских данных, сюда пихайте что хотите
  // другие ключи запрещены по спеке!
}

Первое что бросается в глаза так это то, что GraphQL возвращает массив ошибок. Wow! Т.к. запрос может быть сложный с запросом кучи ресурсов, то GraphQL может вернуть вам часть данных, а на оставшуюся часть вернуть ошибки. И это хорошо, пол ответа лучше, чем ничего.

Фатальные ошибки

Фатальная ошибка чаще всего имеет следующий вид — 500 Internal Server Error. Возникает обычно если кончилась память, забыли установить пакет, совершили грубую синтаксическую ошибку в коде. Да много еще чего. При этом дело не доходит до обработки GraphQL-запроса. И здесь резонно вернуть 500 ошибку.

Нет работы GraphQL, нет кода 200.

Фронтендеры обычно это дело должны обрабатывать на уровне своего Network Layer’a. Получили 500, значит где-то косячнулись бэкендеры с админами.

Ошибки валидации

Сервер получил запрос и делегировал его в пакет graphql. Перед тем как GraphQL-запрос будет выполняться он проходит парсинг и валидацию. Если кривой запрос, то никакие resolve-методы вызваны не будут и тупо будет возвращена ошибка:

{
  errors: [
    {
      message: 'Cannot query field "wrong" on type "Query".',
      locations: [{ line: 3, column: 11 }],
    },
  ],
}

// или например такая
{
  errors: [
    {
      message: 'Variable "$q" of required type "String!" was not provided.',
      locations: [{ line: 2, column: 16 }],
    },
  ],
}

При этом сервер вернет статус 200. При коде 200, ошибка обычно на стороне фронтендера. Но и бекендер может быть к этому причастен, если взял и удалил из схемы какое-то поле. В таком случае все старые работающие приложения теперь стали отправлять невалидные запросы.

Runtime ошибки в resolve-методах

Если запрос прошел парсинг и валидацию, то он начинает выполняться и вызывать resolve-методы вашей схемы согласно присланному GraphQL-запросу. И если вдруг внутри resolve-метода вываливается Exception (throw new Error()), неважно явно вы его выбросили, или он прилетел из недр чужих пакетов. То происходит следующая магия:

  • обработка ветки графа приостанавливается (вложенные resolve-методы вызываться не будут)
  • на месте элемента, где произошла ошибка возвращается null
  • ошибка добавляется в массив errors
  • НО при этом соседние ветки продолжают работать

Хорошо это понять можно на примере следующего кода:

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: {
      search: {
        args: {
          q: { type: GraphQLString },
        },
        resolve: (_, args) => {
          if (!args.q) throw new Error('missing q');
          return { text: args.q };
        },
        type: new GraphQLObjectType({
          name: 'Record',
          fields: {
            text: {
              type: GraphQLString,
              resolve: source => source.text,
            },
          },
        }),
      },
    },
  }),
});

const res = await graphql({
  schema,
  source: `
    query {
      s1: search(q: "ok") { text }
      s2: search { text }
      s3: search(q: "good") { text }
    }
  `,
});

Ответ от сервера будет получен следующий:

{
  errors: [
    { message: 'missing q', locations: [{ line: 4, column: 11 }], path: ['s2'] }
  ],
  data: { s1: { text: 'ok' }, s2: null, s3: { text: 'good' } },
}

Поле s1 возвращает полный результат. В s2 была выброшена ошибка, поэтому оно стало null и в массив errors добавилась ошибка. И дальше поле s3 тоже без проблем вернулось.

Т.е. получается на тех местах, где была выброшена ошибка возвращается null и пишется ошибка в массив. А вся остальная часть запроса продолжает выполняться как ни в чем не бывало. Вот такой вот он добрый GraphQL, хоть что-нибудь да вернет.

Точно также работает, если бэкендер вернул данные неправильного типа в resolve-методе. GraphQL не позволяет вернуть «левые данные» в data.

Вот пример, когда мы по схеме должны вернуть массив строк, но второй элемент не является строкой. Вместо «левого» значения, он вернет null и при этом добавит ошибку в массив:

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: {
      ooops: {
        type: new GraphQLList(GraphQLString),
        resolve: () => ['ok', { hey: 'wrong non String value' }],
      },
    },
  }),
});

const res = await graphql(schema, `query { ooops }`);

expect(res).toEqual({
  errors: [
    {
      message: 'String cannot represent value: { hey: "wrong non String value" }',
      locations: [{ line: 3, column: 11 }],
      path: ['ooops', 1],
    },
  ],
  data: { ooops: ['ok', null] },
});

Также спецификация GraphQL позволяет передать дополнительные данные вместе с ошибкой через проперти extensions. Давайте создадим объект ошибки и присвоим ему два проперти extensions и someOtherData:

new GraphQLObjectType({
  name: 'Query',
  fields: {
    search: {
      resolve: () => {
        const e: any = new Error('Some error');
        e.extensions = { a: 1, b: 2 }; // will be passed in GraphQL-response
        e.someOtherData = { c: 3, d: 4 }; // will be omitted
        throw e;
      },
      type: GraphQLString,
    },
  },
});

На выходе в GraphQL-ответе мы получим следующие данные (extensions будет передан, а все другие проперти из объекта ошибки будут опущены, например не будет someOtherData из нашего примера):

{
  errors: [
    {
      message: 'Some error',
      locations: [{ line: 1, column: 9 }],
      path: ['search'],
      extensions: { a: 1, b: 2 },
    },
  ],
  data: { search: null },
}

Такой механизм позволяет передать клиентам дополнительные данные об ошибке.

Ну коль заговорили про фронтенд, давайте пофантазируем как им работать с такими ошибками. На верхнем уровне одну ошибку в модальном окне вывести не проблема, а если ошибок две? А если у нас сложное приложение и ошибки надо показывать в разных частях приложения? Вот тут у фронтендера начинается просто адская боль и печаль с таким массивом ошибок. Его надо отдельно парсить, понимать какая именно ошибка произошла (например через extensions.code). Как-то передать ошибку в нужную компоненту и на нужный уровень. В общем, приходится сильно изгаляться в коде пробросом лишних проперти и логикой.

Если вам интересно как бэкендер может упростить жизнь фронтендеру, то обязательно читайте следующий раздел.

Пользовательские ошибки

Что такое пользовательские ошибки? Ну это когда вам где-то в приложении надо вывести «запись не найдена», или «у вас нет прав просматривать этот контент», или «необходимо подтвердить возраст» или в списке на 23 элементе показать что «запись удалена».

Если пользоваться стандартным механизмом ошибок GraphQL. То на фронтенде приходится сильно изгаляться, чтобы пробросить ошибку в нужное место.

Но эту проблему можно достаточно элегантно решить, если ошибки возвращать прямо в data на нужном уровне, а не через глобальный массив errors. Для этого в GraphQL есть Union-типы, которые возвращают либо запись с данными, либо ошибку.

Давайте сразу к живому примеру. Представим что нам надо вернуть список видео. Причем какие-то видео в обработке, другие перед просмотром необходимо купить или подтвердить свой возраст. Так давайте и будем возвращать список, который может вернуть Union-тип из Video, VideoInProgressProblem, VideoNeedBuyProblem и VideoApproveAgeProblem. Со стороны фронтендера можно тогда написать вот такой запрос:

query {
  list {
    __typename # <----- магическое поле, которое вернет имя типа для каждой записи
    ...on Video {
      title
      url
    }
    ...on VideoInProgressProblem {
      estimatedTime
    }
    ...on VideoNeedBuyProblem {
      price
    }
    ...on VideoApproveAgeProblem {
      minAge
    }
  }
}

Т.е. используем фрагменты на конкретных типах и запрашиваем поле __typename, которое возвращает имя типа. К запросу выше GraphQL-ответ будет следующий:

{
  data: {
    list: [
      { __typename: 'Video', title: 'DOM2 in the HELL', url: 'https://url' },
      { __typename: 'VideoApproveAgeProblem', minAge: 21 },
      { __typename: 'VideoNeedBuyProblem', price: 10 },
      { __typename: 'VideoInProgressProblem', estimatedTime: 220 },
    ],
  },
}

При таком подходе фронтендер знает какие вообще ошибки могут быть. Также он получает ошибки в нужной компоненте, на нужном уровне. Код захламляется только там, где необходимо разобрать разные варианты пользовательских ошибок и вывести либо данные, либо красивый блок с ошибочкой.

Причем фронтендеры могут легко понять, какой тип ошибки вернулся. И при этом получить дополнительные данные по ошибке, если она их возвращает. Это же просто обычный тип в схеме, который может содержать в себе любые необходимые поля.

Для себя я вынес одно правило, что пользовательским ошибкам лучше всего давать суффикс Problem, а не Error. Это позволяет избежать путаницы как на бэкенде, так и на фронтенде.

Как это дело можно организовать на бэкенде? Достаточно просто. Вот пример:

// Объявляем класс Видео
class Video {
  title: string;
  url: string;

  constructor({ title, url }) {
    this.title = title;
    this.url = url;
  }
}

// И сразу же объявим GraphQL-тип
const VideoType = new GraphQLObjectType({
  name: 'Video',
  fields: () => ({
    title: { type: GraphQLString },
    url: { type: GraphQLString },
  }),
});


// Объявим классы проблем (ошибок)
class VideoInProgressProblem {
  constructor({ estimatedTime }) {
    this.estimatedTime = estimatedTime;
  }
}
class VideoNeedBuyProblem {
  constructor({ price }) {
    this.price = price;
  }
}
class VideoApproveAgeProblem {
  constructor({ minAge }) {
    this.minAge = minAge;
  }
}

// И их типы для GraphQL
const VideoInProgressProblemType = new GraphQLObjectType({
  name: 'VideoInProgressProblem',
  fields: () => ({
    estimatedTime: { type: GraphQLInt },
  }),
});
const VideoNeedBuyProblemType = new GraphQLObjectType({
  name: 'VideoNeedBuyProblem',
  fields: () => ({
    price: { type: GraphQLInt },
  }),
});
const VideoApproveAgeProblemType = new GraphQLObjectType({
  name: 'VideoApproveAgeProblem',
  fields: () => ({
    minAge: { type: GraphQLInt },
  }),
});

// Ну а теперь самое интересное.
// Объявляем наш UNION-тип который будет возвращать либо видео, либо проблему-ошибку
const VideoResultType = new GraphQLUnionType({
  // Даем имя типу.
  // Здорово если если вы выработаете конвенцию в своей команде
  // и к таким Union-типам будете добавлять суффикс Result
  name: 'VideoResult',

  // как хорошие бекендеры добавляем какое-нибудь описание
  description: 'Video or problems',

  // объявляем типы через массив, которые могут быть возвращены
  types: () => [
    VideoType,
    VideoInProgressProblemType,
    VideoNeedBuyProblemType,
    VideoApproveAgeProblemType,
  ],

  // Ну и самое главное надо объявить функцию определения типа.
  // resolve-функции (смотри ниже поле Query.list) просто возвращают JS-объект
  // но вот GraphQL'ю нужно как-то JS-объект, сконвертировать в GraphQL-тип
  // иначе как он узнает что надо записать в поле __typename
  resolveType: value => {
    if (value instanceof Video) {
      return VideoType;
    } else if (value instanceof VideoInProgressProblem) {
      return VideoInProgressProblemType;
    } else if (value instanceof VideoNeedBuyProblem) {
      return VideoNeedBuyProblemType;
    } else if (value instanceof VideoApproveAgeProblem) {
      return VideoApproveAgeProblemType;
    }
    return null;
  },
});

// Ну и вишенка на торте
// Пишем простую схемку, которая нам возвращает массив из Видео и Ошибок-Проблем.
const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: {
      list: {
        type: new GraphQLList(VideoResultType),
        resolve: () => {
          return [
            new Video({ title: 'DOM2 in the HELL', url: 'https://url' }),
            new VideoApproveAgeProblem({ minAge: 21 }),
            new VideoNeedBuyProblem({ price: 10 }),
            new VideoInProgressProblem({ estimatedTime: 220 }),
          ];
        },
      },
    },
  }),
});

Очень просто и красиво. А самое главное удобно для фронтендеров:

  • знают какие ошибки могут быть
  • знают какие поля содержатся в ошибках
  • отлично поддерживается статический анализ, в отличии от обычных ошибок
  • ошибки возвращаются в дереве ответа, а не в глобальном массиве
  • в результате чище, проще и безопаснее код

Любите брата фронтендера своего 😉 Иначе они придут с вилами!

Ссылки по теме

  • Примеры кода в виде тестов к этой статье
  • Видео про ошибки от Sasha Solomon
  • Похожее видео про ошибки от Eloy Durán, всё-таки у Саши лучше

If execution of a GraphQL query in your Scala application goes wrong for whatever reasons, Sangria will respond to the client with the message Internal server error, which is hardly helpful.

Default error handling in Scala applications powered by Sangria isn’t really practical. But Sangria does provide us with the functionality to handle errors in a way that makes sense. In this article, we look at a few examples of managing exceptions when GraphQL queries can’t be parsed or properly executed.

We take a look at these aspects of managing errors with Sangria:

  • Handling errors before a query is executed
  • Handling errors when a query is executed
  • Creating custom error messages
  • Managing violations of GraphQL queries
  • Handling errors from custom QueryReducer

You may want to familiarize yourself with the official error format in GraphQL documentation. Also, check out our Scala application with properly implemented GraphQL error handling in this repository.

Our app uses a typical Scala technology stack — Play Framework, SBT, and Guice. You can download and run the app and then run a few GraphQL mutations and queries to see what errors are produced (we show a few examples further in the article as well). Also, have a look at the application structure below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
scala-graphql-api
├── app                    # The Scala application source code
│   ├── controllers        # Contains AppController
│   ├── graphql            # GraphQL schemas and resolvers
│   │   ├── resolvers      # Resolver methods to execute GraphQL queries
│   │   ├── schemas        # Concrete schemas, in particular, PostSchema
│   │   └── GraphQL.scala  # Defines global GraphQL-related objects
│   ├── models             # The application models
│   │   ├── errors         # A folder that contains models of various error classes
│   │   └── Post.scala     # The Post entity model
│   ├── modules            # The modules such as PostModule and DBModule
│   ├── repositories       # Contains the trait PostRepository with its implementation
│   ├── validators         # Contains the trait PostValidator with its implementation
│   └── views              # HTML layouts (a graphiql layout)
├── conf
├── project
├── public
├── test
├── .gitignore
├── .travis.yml
├── build.sbt
└── README.md

As shown in the application structure, we’ll be working with the Post entity, which has the following shape:

1
case class Post(id: Option[Long] = None, title: String, content: String)

With the obvious stuff out of the way, you can step into the realm of Sangria and GraphQL error handling.

Defining HTTP status codes in GraphQL applications

Before we discuss how to manage GraphQL errors with Sangria, we want to clarify an essential aspect of how GraphQL-based applications work with HTTP status codes.

We’re able to set HTTP status codes for errors thrown during the GraphQL query validation before the query is even executed. Basically, it means that for a concrete validation error we can specify a concrete HTTP status code to be returned to the client.

However, errors thrown during query execution (read: inside the GraphQL resolver methods) are always returned by the GraphQL server as an HTTP response with the status code 200, unlike in a RESTful API.

In REST, each request is handled separately and so we can specify the status code for each request also separately. But in a GraphQL API, we can’t use the same approach because several queries and mutations can be sent in a single client request. Some queries or mutations may result in error; others may not. This is why we have to always return the HTTP status code 200. You’ll see an example of this in the section Handling errors when executing GraphQL queries.

Handling errors before executing a GraphQL query

Upon receiving a GraphQL query from the client, Sangria analyzes the request and validates it. Sangria defines several
types of errors that can happen before the query is executed:

  • QueryReducingError, a wrapper for errors thrown from QueryReducer (which we discuss later).
  • QueryAnalysisError, a wrapper for errors with the GraphQL query or variables. These errors are made by the client application.
  • ErrorWithResolver, a basic trait for handling QueryAnalysisError and QueryReducingError or any other unexpected errors.

To clarify how they are connected, have a look at the inheritance chain:

1
QueryReducingError <= QueryAnalysisError <= ErrorWithResolver

And the very first code sample that demonstrates the use of Sangria functionality, in particular, the listed error types, is this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def executeQuery(query: String, variables: Option[JsObject] = None, operation: Option[String] = None): Future[Result] = QueryParser.parse(query) match {
 case Success(queryAst: Document) => Executor.execute(
   schema = graphQL.Schema,
   queryAst = queryAst,
   variables = variables.getOrElse(Json.obj()),
 ).map(Ok(_)).recover {
   case error: QueryAnalysisError => BadRequest(error.resolveError)
   case error: ErrorWithResolver => InternalServerError(error.resolveError)
 }
 case Failure(ex) => Future(
   BadRequest(
     JsObject(
       Seq(
         "error" -> JsString("Unable to parse the query.")
       )
     )
   )
 )
}

In this example, if a GraphQL query isn’t valid (an error was made on the client), we return the 400 status code
(BadRequest). And if some other error is thrown before the query is executed, we return the 500 status code
(InternalServerError).

The key aspect of GraphQL error handling in this case is the use of built-in methods from Sangria. Notice how we passed
error.resolveError into the BadRequest() and InternalServerError(). The method resolveError() is exposed
by the error trait ErrorWithResolver and renders the exception in the format compliant with GraphQL.

As you can see, managing exceptions before executing GraphQL queries it’s quite simple with Sangria.

If only errors were made in the incoming GraphQL queries, we’d finish our article right now. But errors can also be
produced when the queries are executed, and in the next section we explain how to handle them.

Handling errors when executing GraphQL queries

What happens after a GraphQL query has been successfully validated? Naturally, it gets executed by a respective resolve function, and various errors can be produced at this stage.

Consider a situation when the client application tries to befool our Scala server by sending a post with a title that was already used. We don’t want to store two or more posts with the same title so we need Sangria to decline the transaction and return an error.

This is what a basic mutation (defined in app/graphql/schema/PostSchema.scala in our app) to add a new post looks
like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val Mutations: List[Field[Unit, Unit]] = List(
 Field(
   name = "addPost",
   fieldType = PostType,
   arguments = List(
     Argument("title", StringType),
     Argument("content", StringType)
   ),
   resolve = sangriaContext =>
     postResolver.addPost(
       sangriaContext.args.arg[String]("title"),
       sangriaContext.args.arg[String]("content")
     )
 )
)

resolve() runs the addPost() method defined in the PostResolver class located in app/graphql/resolvers. Here’s
the addPost() implementation:

1
def addPost(title: String, content: String): Future[Post] = postRepository.create(Post(title = title, content = content))

As you can see, we can’t but make everything complicated. PostResolver.addPost() doesn’t really do anything other than
invoking the method create() implemented in the PostRepositoryImpl class, located in app/repositories next to the
PostRepository trait it extends.

Here’s the method create() from PostRepositoryImpl (and we still don’t see how it’s implemented):

1
2
3
override def create(post: Post): Future[Post] = db.run {
  Actions.create(post)
}

It calls Actions.create() (the Actions object is also defined in PostRepositoryImpl). Finally, we reached the actual method that does something useful.

1
2
3
4
5
6
7
8
9
10
def create(post: Post): DBIO[Post] = for {
 maybePost <- if (post.id.isEmpty) DBIO.successful(None) else find(post.id.get)
 _ <- maybePost.fold(DBIO.successful()) {
     _ => DBIO.failed(AlreadyExists(s"Post with id = ${post.id} already exists."))
   }
 postWithSameTitle <- postQuery.filter(_.title === post.title).result
 id <- if (postWithSameTitle.lengthCompare(1) < 0) postQuery returning postQuery.map(_.id) += post else {
     DBIO.failed(AlreadyExists(s"Post with title = '${post.title}' already exists."))
   }
 } yield post.copy(id = Some(id))

In the code snippet above, we filter posts by title and if the application doesn’t find a post with the title sent by the client, then a new post is added to the database. However, if a post with the same title already exists, we return an error of type AlreadyExists, which is a case class located in app/models/errors.

Here’s the implementation of AlreadyExists:

1
2
3
case class AlreadyExists(msg: String) extends Exception with UserFacingError {
  override def getMessage(): String = msg
}

This class demonstrates one of the key aspects of error handling. AlreadyExists implements the trait UserFacingError
provided by Sangria so that our Scala application returns a meaningful message to the client.

The error Post with title = '{title}' already exists. will be sent in the field errors in the created GraphQL response (remember that the status code will still be 200 as we discussed in the section Defining HTTP status codes in GraphQL applications).

Error handling with the UserFacingError trait in a Scala GraphQL application

To show you how exactly the UserFacingError trait is useful, try to remove the with UserFacingError part in app/models/errors/AlreadyExists.scala. Sangria will return a boring message Internal server error instead of our custom message.

Error handling without the UserFacingError trait in a Scala GraphQL application

Sangria gives us a way to implement our own, more advanced mechanism for handling errors than the use of the
UserFacingError trait.

Consider a typical development problem: before your application saves a new post, you may want it to validate the post
title length or characters. What ingredients do we need to validate the title? It depends on a concrete implementation.
Our validation solution consists of the following elements:

  • The InvalidTitle error model
  • The PostValidator trait with a defined validation method
  • The PostValidatorImpl class that implements the validation method

We first define a model for validation errors. Our case class can be called InvalidTitle (in our application, this class is stored in the app/models/errors folder):

1
case class InvalidTitle(msg: String) extends Exception(msg)

Pay attention that this time our class doesn’t implement the trait UserFacingError as did AlreadyExists. We’ll use another mechanism provided by Sangria for more advanced error handling.

Let’s now create a wrapper method that accepts two parameters: the title and the callback function. We invoke the callback function if the title is valid and return an error otherwise.

This method is implemented in PostValidatorImpl class that you can find in app/validators:

1
2
3
4
5
6
7
class PostValidatorImpl extends PostValidator {
  val titleRegex = "[a-zA-Z0-9- ]{3, 100}"
  
  override def withTitleValidation[T](title: String)(callback: => Future[T]): Future[T] = {
    if (title.matches(titleRegex)) callback else Future.failed(InvalidTitle("The post's title is invalid."))
  }
}

Now we need to go back to the method addPost() in app/graphql/resolvers/PostResolver.scala and just wrap the invoked method postRepository.create() with withTitleValidation().

1
2
3
4
5
def addPost(title: String, content: String): Future[Post] = {
  withTitleValidation(title) {
    postRepository.create(Post(title = title, content = content))
  }
}

But that’s not all we must do to handle the error with withTitleValidation() in our Scala app. We need to create an instance of Sangria’s class ExceptionHandler and then pass it to the Executor as an optional parameter to handle the errors such as InvalidTitle.

In our application, exceptionHandler is created in app/graphql/GraphQL.scala, but it’s recommended that you move it to another place in your project.

1
2
3
4
5
val exceptionHandler = ExceptionHandler(
  onException = {
    case (resultMarshaller, error: InvalidTitle) => HandledException(error.getMessage)
  }
)

Add a finishing stroke: pass the created exceptionHandler to the Executor (it’s located in app/controllers/AppController.scala):

1
2
3
4
5
6
Executor.execute(
  schema = graphQL.Schema,
  queryAst = queryAst,
  variables = variables.getOrElse(Json.obj()),
  exceptionHandler = graphQL.exceptionHandler
)

With this implementation, our Scala and GraphQL server will return a meaningful error instead of Internal Server Error (provided that its type is InvalidTitle). Now, if you try to send a mutation with a title that has a forbidden character, you’ll see a message similar to this:

Advanced validation error handling with Sangria in a Scala application

The error message "Post's title is invalid." is a bit vague, though. We can improve it by adding more fields to
HandleException to return more data to the client.

1
2
3
4
5
6
7
8
val exceptionHandler = ExceptionHandler(
  onException = {
    case (resultMarshaller, error: InvalidTitle) => HandleException(
      error.getMessage,
      Map("validation_rule" -> resultMarshaller.fromString("Allowed characters: a-z, A-Z, 0-9, and -. The length must be 3 up to 100 characters."))
    )
  }
)

Now the response will have an additional field validation_rule which will contain a more advanced description of the error inside the field extensions:

Advanced error message with Sangria in a Scala application

We can also simplify the error format by reducing the level of nesting. Using sane words, we just want to add the field validation_rule on the same level as the message and get rid of extensions. To do that, pass additional boolean arguments addFieldsInError and addFieldsInExtensions with respective values in HandledException:

1
2
3
4
5
6
7
8
9
10
11
12
val exceptionHandler = ExceptionHandler(
 onException = {
   case (resultMarshaller, error: InvalidTitle) => HandledException(
     error.getMessage,
     Map(
       "validation_rule" ->resultMarshaller.fromString("Allowed characters: a-z, A-Z, 0-9, and -. The length must be 3 up to 100 characters.")
     ),
     addFieldsInError = true,
     addFieldsInExtensions = false
   )
 }
)

And this is what the response will look like after the last change:

Advanced error message with Sangria in a Scala application

Sangria also allows us to pass several errors into HandledException using the multiple() method to produce several errors. You can add the code below to the file app/graphql/GraphQL.scala in our Scala application.

1
2
3
4
5
6
HandledException.multiple(
  Vector(
    ("Error #1", Map("errorCode" -> resultMarshaller.fromString("OOPS!!!")), Nil),
    ("Error #2", Map.empty[String, resultMarshaller.Node], Nil),
  )
)

And here’s the result:

Multiple error messages with Sangria in a Scala application

With the last change, you can get several errors in the errors array.

Violations handling in Sangria and GraphQL application

Sangria allows us to catch violations such as the validation errors of the incoming query. If the client sends a request with an incorrect mutation name, say addPos instead of addPost registered in the schema, Sangria will handle this error as a standard query analysis error:

We can override this behavior to get more control over how the error is handled. For example, we can extend our exception hanlder defined in app/graphql/GraphQL.scala by adding the onViolation function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
val exceptionHandler = ExceptionHandler(
 onException = {
   case (resultMarshaller, error: InvalidTitle) => HandledException(
     error.getMessage,
     Map(
       "validation_rule" ->resultMarshaller.fromString("Allowed characters: a-z, A-Z, 0-9, and -. The length must be 3 up to 100 characters.")
     ),
     addFieldsInError = true,
     addFieldsInExtensions = false
   )
 },
 onViolation = {
   case (resultMarshaller, violation: UndefinedFieldViolation) =>
     HandledException("Field is missing!",
       Map(
         "fieldName"  resultMarshaller.fromString(violation.fieldName),
         "errorCode"  resultMarshaller.fromString("FIELD_MISSING"))
     )
 }
)

With this implementation, another result will be returned to the client:

The error Field Missing in a GraphQL query implemented with Sangria

Handling errors from Sangria’s QueryReducer

A GraphQL schema can have circular dependencies to let the client application send infinitely deep queries. But allowing this is a very bad idea because we can easily overload the server. Thankfully, Sangria provides a way to protect our GraphQL server against malicious queries using QueryReducer.

QueryReducer is an object that enables us to configure the parameters for protection against malicious queries. We can pass a list of QueryReducer objects to Executor so that Sangria will analyze the incoming GraphQL query and only then execute it if everything’s fine.

Sangria provides two mechanisms to protect against malicious queries. We can limit the query complexity and the query
depth
. In simple terms, the query complexity means how great the GraphQL query is, and the query depth defines the maximal nesting level in a GraphQL query. You can find more information about these mechanisms in the official Sangria documentation.

To use both these mechanisms in our Scala application, we defined two constants in GraphQL.scala. One constant is called maxQueryDepth and is set to 15. The other constant is maxQueryComplexity and it’s set to 1000. Now we need to add a list of QueryReducer objects in Executor and pass the constants in them:

1
2
3
4
5
6
7
8
9
10
Executor.execute(
 schema = graphQL.Schema,
 queryAst = queryAst,
 variables = variables.getOrElse(Json.obj()),
 exceptionHandler = graphQL.exceptionHandler,
 queryReducers = List(
   QueryReducer.rejectMaxDepth[Unit](graphQL.maxQueryDepth),
   QueryReducer.rejectComplexQueries[Unit](graphQL.maxQueryComplexity, (_, _) => TooComplexQueryError)
 )
)

Then, we handle these two errors in the onException function in ExceptionHandler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
val exceptionHandler = ExceptionHandler(
    onException = {
      case (resultMarshaller, error: InvalidTitle) => HandledException(
        error.getMessage,
        Map(
          "validation_rule" -> resultMarshaller.fromString("Allowed characters: a-z, A-Z, 0-9, and -. The length must be 3 up to 100 characters.")
        ),
        addFieldsInError = true,
        addFieldsInExtensions = false
      )
      case (_, error: TooComplexQueryError) => HandledException(error.getMessage)
      case (_, error: MaxQueryDepthReachedError) => HandledException(error.getMessage)
    },
    // ...

Note that QueryReducer.rejectMaxDepth provides a standard error message. QueryReducer.rejectComplexQueries(), however, requires an additional error class. We created a respective model TooComplexQueryError for this kind of errors under app/models/errors:

1
case class TooComplexQueryError(msg: String = "Query is too expensive.") extends Exception(msg)

Now, if we, for example, send a query with depth exceeding what we defined, the client will receive the following error message:

Query depth error with GraphQL and Sangria in a Scala application

For this example, we actually set maxQueryDepth to just 1 for the sake of simplicity. In your real application, you should set depth to, say, 15, just like we did in GraphQL.scala in our Scala app.


That’s all the basics you need to know about error handling in Scala and GraphQL API using Sangria.

If you’re looking for a developer or considering starting a new project,

we are always ready to help!

Contact Us

Masa

Masa

Posted on May 5, 2019

If you want to develop a stable, high availablility application, error-handling is something you have to pay great attention to.
GraphQL is a fairly new protocol compared to RESTful and it should be dealt with completely different approaches given its unique way to communicate with clients.
As I started developing some application with Ruby on Rails and graphql-ruby, I realized that you don’t see many articles on this topic yet, so I decided to share the way I do error-handling with GraphQL and Ruby on Rails.

First I’ll quickly explain GraphQL’s specification and practices, then go on to the main topic which is how I implement error-handling with graphql-ruby.

table of contents

  • GraphQL’s specification and practices
  • The variety of errors you get with GraphQL
  • The implementation with graphql-ruby
  • safety net with rescue_from method
  • handling multiple errors with add_error method

GraphQL’s specification and practices

practices on response’s format in GraphQL

One of the GraphQL’s practices is that every response is returned with http status 200 and occurred errors are included errors key in the response body.
That is because you can include multiple queries in a request in GraphQL.

Since GraphQL allows for multiple operations to be sent in the same request, it’s well possible that a request only partially fails and returns actual data and errors.

https://www.graph.cool/docs/faq/api-eep0ugh1wa/#how-does-error-handling-work-with-graphcool

This is a practice not specification but related tools like Apollo and graphql-ruby follow this practice and that makes us follow it too.

GraphQL’s specification about response’s format

Then how do you express errors in GraphQL?
Let’s take a look at the GraphQL’s specification here.

https://facebook.github.io/graphql/June2018/#sec-Errors

According to the GraphQL’s specification, response’s format is Hash with keys named data and errors.
data is the key that has the actual data and errors is the key that has errors occurred during the execution.

{
  "errors": [
    {
      "message": "hogehoge",
      "extensions": {
        "bar": "bar"
      }
    }
  ],
  "data": {
    "user": {
      "name": "Jon"
    }
  }
}

By the way, the format of a hash inside errors is specified in specification as it has 3 keys named message, location and path.
If you wanna have it have another key, create a key named extensions and put your key in it like shown below.

{
  "errors": [
    {
      "message": "Name for character with ID 1002 could not be fetched.",
      "locations": [ { "line": 6, "column": 7 } ],
      "path": [ "hero", "heroFriends", 1, "name" ],
      "extensions": {
        "code": "CAN_NOT_FETCH_BY_ID",
        "timestamp": "Fri Feb 9 14:33:09 UTC 2018"
      }
    }
  ]
}

You want to add your own key this way because it might potentially conflicts with keys added in the future if you create your key at the same level as other default keys like message and line.

GraphQL services should not provide any additional entries to the error format since they could conflict with additional entries that may be added in future versions of this specification.

https://facebook.github.io/graphql/June2018/#sec-Errors

The variety of errors you get with GraphQL

Alright, now we know how to express errors in GraphQL.
Next, let’s see what kind of errors you can get with GraphQL.
I’m referencing the Apollo’s article on error-handling.

Full Stack Error Handling with GraphQL + Apollo 🚀 – Apollo GraphQL

Those are the 2 perspectives we use to categorize errors.

  • Is it the client or server that is at fault?
  • Where did the error occur?

We can categorize client errors into 3 types.

  • parse error
    • query syntax error
  • validation error
    • error at the type check phase
  • execution error
    • authentication error

Server-side errors are the errors occurred in Rails API codes.

Now that we found out what kind of errors are out there, let’s see how we can express them in responses.
We want to unify the format of all the responses as specified in the specification so that client tools like Apollo can parse them easily.
At this time, we will put detailed error messages into message key and put a key named code into extensions key which represents a status code.
This is actually the same format as that of the example response in the specification.

If you get an error at authentication phase, the response will look like this.

"errors": [
    {
      "message": "permission denied",
      "locations": [],
      "extensions": {
        "code": "AUTHENTICATION_ERROR"
      }
    }
  ]

If an error occurred in Rails API codes, the response will look like this.

"errors": [
    {
      "message": "undefined method 'hoge' for nil",
      "locations": [],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR"
      }
    }
  ]

The implementation with graphql-ruby

Now that you know how to express errors in responses, let’s see how you can implement those with graphql-ruby.

how do you catch and return errors with graphql-ruby?

You can put an error into errors key in the response by raising GraphQL::ExecutionError like this.

def resolve(name:)
  user = User.new(name: name)
  if user.save
    { user: user }
  else
    raise GraphQL::ExecutionError, user.errors.full_messages.join(", ")
  end
end

Authentication error

Let’s take a look at this example showing how to deal with an authentication error.

This example is based on the premise that you can get your login session by calling current_user method like this.

class GraphqlController < ApplicationController
  def execute
    variables = ensure_hash(params[:variables])
    query = params[:query]
    operation_name = params[:operationName]
    context = { current_user: current_user }
    result = SampleSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
    render json: result
    #...
  end
  #...
end

Then you raise GraphQL::ExecutionError with the error code AUTHENTICATION_ERROR depends on the value of context[:current_user] in the resolve method.

def resolve(name:, sex:)
  raise GraphQL::ExecutionError.new('permission denied', extensions: { code: 'AUTHENTICATION_ERROR' }) unless context[:current_user]

  #...
end

With this implementation, you get a response like this when an authentication error occurred.

"errors": [
    {
      "message": "permission denied",
      "locations": [
        {
          "line": 3,
          "column": 3
        }
      ],
      "path": [
        "createUser"
      ],
      "extensions": {
        "code": "AUTHENTICATION_ERROR"
      }
    }
  ]

safety net with rescue_from method

As you may know by now, it is important to unify the format of responses so that the clients can parse them easily.
If an occurred error is not rescued, general 500 internal server error is returned to the clients which forces them to be prepared to get 2 types of responses.
Obviously you do not want that to happen because you want the clients to be comfortable with handling responses.
With graphql-ruby, you can make sure responses are in the same format with rescue_from method.

class SampleSchema < GraphQL::Schema
  rescue_from(StandardError) do |message|
    GraphQL::ExecutionError.new(message, extensions: {code: 'INTERNAL_SERVER_ERROR'})
  end

#...
end

The response will look like this.

"errors": [
    {
      "message": "hogehoge",
      "extensions":  {
        "code": "INTERNAL_SERVER_ERROR"
      }
    }
  ]

By the way, I wrote the patch to pass Error class objects to the method :)

extend GraphQL::Schema::RescueMiddleware#attempt_rescue by masakazutakewaka · Pull Request #2140 · rmosolgo/graphql-ruby · GitHub

Well it’s not included in a stable blanch yet so technically you can only pass a String object to the method at the moment though…

Keep in mind that this rescue_from method is unavailable from version 1.9.
That is because GraphQL::Schema::RescueMiddleware class is not supported in version 1.9 which is where rescue_from method is defined.
GraphQL — Interpreter

You don’t have a substitute for the method in version 1.9 and we don’t even know what the additional feature to replace it might look like at the moment.
GraphQL::Execution::Interpreter and rescue_from compatibility · Issue #2139 · rmosolgo/graphql-ruby · GitHub

handling multiple errors with add_error method

You want to bundle up all the occurred errors in a response in some cases.
One of the cases is when you have a mutation with multiple user inputs like a sign up interface.
With graphql-ruby, you can bundle up multiple errors with add_error method.

module Mutations
  class CreateUser < GraphQL::Schema::RelayClassicMutation
    argument :name, String, required: true
    argument :sex, String, required: true

    field :user, Types::UserType, null: true

    def resolve(name:, sex:)
      user = User.new({ name: name, sex: sex })
      if user.save
        { user: user }
      else
        build_errors(user)
        return # rescue_from is invoked without this
      end
    end

    def build_errors(user)
      user.errors.map do |attr, message|
        message = user[attr] + ' ' + message
        context.add_error(GraphQL::ExecutionError.new(message, extensions: { code: 'USER_INPUT_ERROR', attribute: attr }))
      end
    end
  end
end

The response with multiple errors in it will look like this.

"errors": [
    {
      "message": "hoge already exists",
      "extensions": {
        "code": "USER_INPUT_ERROR",
        "attribute": "name"
      }
    },
    {
      "message": "fuge already exists",
      "extensions": {
        "code": "USER_INPUT_ERROR",
        "attribute": "sex"
      }
    }
  ]

On a side note, there is another way to achieve this which is to define the GraphQL type for the error and put errors in data key.
https://github.com/rmosolgo/graphql-ruby/blob/master/guides/mutations/mutation_errors.md#errors-as-data

conclusion

In this article, I introduced the idea on how error-handling is done in GraphQL and the implementation of it using graphql-ruby.
GraphQL’s specification is put together well and not too long to read so I recommend you have a read.
Hopefully this article can be helpful for those who are grappling with error-handling in GraphQL right now.

Lastly, I’m looking for a job in Canada so any messages or contacts are more than welcome!

Email: takewakamma@gmail.com

As long as it’s Canada, I can relocate :)

Thank you for reading!

references

  • GraphQL
  • API | Graphcool Docs
  • Full Stack Error Handling with GraphQL + Apollo 🚀 – Apollo GraphQL
  • graphql-ruby/overview.md at master · rmosolgo/graphql-ruby · GitHub
  • graphql-ruby/execution_errors.md at master · rmosolgo/graphql-ruby · GitHub
  • graphql-ruby/mutation_errors.md at master · rmosolgo/graphql-ruby · GitHub
Skip to content



Open


Issue created Feb 26, 2020 by random name@skarls

GraphQL 500 Internal Server Error : project query

Summary

String contains null byte in Project GraphQL query result in 500 status

Steps to reproduce

Run the following query:
{
project(fullPath: «eu0000») {
name
fullPath
}
}

Example Project

Docker image of 12.6.3-ce

What is the current bug behavior?

500 status code returned

What is the expected correct behavior?

200 status code

Relevant logs and/or screenshots

ArgumentError (string contains null byte):

app/models/concerns/routable.rb:58:in `block in where_full_path_in'

app/models/concerns/routable.rb:57:in `map'

app/models/concerns/routable.rb:57:in `where_full_path_in'

app/graphql/resolvers/full_path_resolver.rb:16:in `block in model_by_full_path'

lib/gitlab/graphql/authorize/authorize_field_service.rb:95:in `allowed_access?'

lib/gitlab/graphql/authorize/authorize_field_service.rb:75:in `filter_allowed'

lib/gitlab/graphql/authorize/authorize_field_service.rb:21:in `block in authorized_resolve'

lib/gitlab/graphql/generic_tracing.rb:40:in `with_labkit_tracing'

lib/gitlab/graphql/generic_tracing.rb:30:in `platform_trace'

lib/gitlab/graphql/generic_tracing.rb:40:in `with_labkit_tracing'

lib/gitlab/graphql/generic_tracing.rb:30:in `platform_trace'

lib/gitlab/graphql/generic_tracing.rb:40:in `with_labkit_tracing'

lib/gitlab/graphql/generic_tracing.rb:30:in `platform_trace'

app/graphql/gitlab_schema.rb:46:in `execute'

app/controllers/graphql_controller.rb:47:in `execute_query'

app/controllers/graphql_controller.rb:18:in `execute'

lib/gitlab/session.rb:11:in `with_session'

app/controllers/application_controller.rb:458:in `set_session_storage'

lib/gitlab/i18n.rb:55:in `with_locale'

lib/gitlab/i18n.rb:61:in `with_user_locale'

app/controllers/application_controller.rb:452:in `set_locale'

lib/gitlab/error_tracking.rb:34:in `with_context'

app/controllers/application_controller.rb:536:in `sentry_context'

lib/gitlab/middleware/rails_queue_duration.rb:27:in `call'

lib/gitlab/metrics/rack_middleware.rb:17:in `block in call'

lib/gitlab/metrics/transaction.rb:62:in `run'

lib/gitlab/metrics/rack_middleware.rb:17:in `call'

lib/gitlab/request_profiler/middleware.rb:17:in `call'

lib/gitlab/middleware/go.rb:20:in `call'

lib/gitlab/etag_caching/middleware.rb:13:in `call'

lib/gitlab/middleware/correlation_id.rb:16:in `block in call'

lib/gitlab/middleware/correlation_id.rb:15:in `call'

lib/gitlab/middleware/multipart.rb:117:in `call'

lib/gitlab/middleware/read_only/controller.rb:48:in `call'

lib/gitlab/middleware/read_only.rb:18:in `call'

lib/gitlab/middleware/basic_health_check.rb:25:in `call'

lib/gitlab/request_context.rb:32:in `call'

config/initializers/fix_local_cache_middleware.rb:9:in `call'

lib/gitlab/metrics/requests_rack_middleware.rb:49:in `call'

lib/gitlab/middleware/release_env.rb:12:in `call'

Completed 500 Internal Server Error in 24ms (Views: 0.1ms | ActiveRecord: 8.3ms | Elasticsearch: 0.0ms)

Hi, I am creating a WordPress GraphQL website using NextJs and Typescript with GraphQL Code Generator.

I am also using URQL as GraphQL client

In a page I am trying to do multiple requests using the hooks provided by GraphQL Code Generator.

const News: React.FC<{}> = ({}) => {
  const classes = useStyles();
  const [{ data: newsData, error }] = usePostsQuery({
    variables: { limit: 8 }
  });

  const [{ data: categoriesData }] = useCategoriesQuery();
  const news = newsData?.posts?.nodes;
  const categories = categoriesData?.categories?.nodes;

  const [selectedCategory, setSelectedCategory] =
    useState<typeof categories[0] | null>(null);
  const [showDropdown, setShowDropdown] = useState<boolean>(false);

  const handleSelectCategory = (category: typeof categories[0]) => {
    setSelectedCategory(category);
  };

  return (
    <Layout>
      ....
    </Layout>
  );
};
export default withUrqlClient(createUrqlClient, { ssr: true })(News);

In Layout I am also fetching the Navbar Menu

const Navbar: React.FC<NavbarProps> = ({}) => {
  const classes = useStyles();
  const [{ data: menuData, error }] = useMenuQuery({
    variables: { location: MenuLocationEnum.HcmsMenuHeader }
  });
  const menuItems = menuData?.menuItems?.nodes;
  const websiteInfo = menuData?.getHeader;

  if (error) {
    console.log(error);
    return <Box>Error Fetching Menu Data</Box>;
  }

  return (
    <div className={classes.root}>
      <Head>
        <title>{websiteInfo?.siteTitle}</title>
        <link rel="shortcut icon" href={websiteInfo?.favicon} />
      </Head>
      ...
    </div>
  );
};

export default Navbar;

This instead is my Urql Client

import { cacheExchange } from "@urql/exchange-graphcache";
import { dedupExchange, fetchExchange } from "urql";

const createUrqlClient = (ssrExchange: any) => ({
  url: "http://localhost:10028/graphql",
  fetchOptions: {
    credentials: "include" as const,
    mode: "no-cors" as const
  },
  exchanges: [
    dedupExchange,
    cacheExchange({
      keys: { HCMSHeader: data => null }
    }),
    ssrExchange,
    fetchExchange
  ]
});

export default createUrqlClient;

The problem is that when I load a Page I get this error 500 in console

POST http://localhost:10028/graphql net::ERR_ABORTED 500 (Internal Server Error)

It seems like I cannot fetch more than one query at once, sometimes it fetches only the navbar menu and it isn’t able to fetch posts and categories on news page.

I think that something is going wrong with the urql cache but I cannot understand what is going on.

Lastly I want to say that all the queries that I am performing work perfectly on the GraphQL playground by executing them one by one and the GraphQL endpoint is correct.

If you have an idea of what I am doing wrong please let me know. Thank you in advance.

Понравилась статья? Поделить с друзьями:

Читайте также:

  • Graphics system window ошибка
  • Graphics error saints row 3
  • Graphics error fortnite mobile
  • Graphics error device removed
  • Graphics driver error cinema 4d

  • 0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии