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 |
|
As shown in the application structure, we’ll be working with the Post entity, which has the following shape:
1
|
|
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 fromQueryReducer
(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 handlingQueryAnalysisError
andQueryReducingError
or any other unexpected errors.
To clarify how they are connected, have a look at the inheritance chain:
1
|
|
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 |
|
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 |
|
resolve()
runs the addPost()
method defined in the PostResolver
class located in app/graphql/resolvers
. Here’s
the addPost()
implementation:
1
|
|
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 |
|
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 |
|
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 |
|
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).
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.
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
|
|
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 |
|
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 |
|
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 |
|
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 |
|
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:
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 |
|
Now the response will have an additional field validation_rule
which will contain a more advanced description of the error inside the field extensions
:
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 |
|
And this is what the response will look like after the last change:
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 |
|
And here’s the result:
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 |
|
With this implementation, another result will be returned to the client:
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 |
|
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 |
|
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
|
|
Now, if we, for example, send a query with depth exceeding what we defined, the client will receive the following error message:
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
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
Open
Issue created Feb 26, 2020 by
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.