Я хочу поделиться своим подходом к обработке ошибок в резолверах GraphQL (я использую GraphQL Yoga на стороне сервера) и передаче их на сторону фронтенда (где я использую Svelte + @urql/svelte).
Бэкенд
Информация об ошибке упаковки
Как я выяснил, единственный способ вернуть ошибку из конечной точки GraphQL — это бросить ошибку в резольвер. Такая конструкция накладывает некоторые ограничения на передачу данных об ошибках между бэкендом и фронтендом. Представим, что пришел какой-то плохой запрос, который сервер не может обработать, и вы хотите вернуть ошибку и, возможно, некоторые подробности о том, что именно пошло не так. Предположим, что мы хотим упаковать всю информацию в следующий интерфейс:
type ApiError = {
code: 400,
message: "The request was bad :("
}
Вернемся к ограничениям — нет способа поместить что-то объектоподобное в качестве аргумента в вызов new Error(arg)
, но мы можем преобразовать объект ApiError
в строку JSON. Это и есть тот странный маленький трюк, который позволяет упаковать любую информацию в бросаемый объект. Это все о передаче информации, но есть еще пара вещей об организации кода.
Возврат ошибки из конечной точки GraphQL путем бросания ошибки может быть удобным и безопасным с точки зрения типов с помощью функций assert в Typescript. Нет необходимости заворачивать операторы возврата в операторы if
; достаточно просто выбросить ошибку и сохранить плоскую структуру кода. Давайте объединим это с упаковкой информации в объект ошибки:
export const newApiError = (errorObject: ApiError) => {
return new Error(JSON.stringify(errorObject))
}
Теперь можно сделать throw newApiError(apiError)
.
Но все еще необходимо проверять, нужно ли нам бросать, что подразумевает операторы if
. Можем ли мы сделать лучше? Да, с помощью функций assert в typescript.
Избавление от утверждений if
Представьте, что бэкенд, получающий запрос на конечной точке, требует авторизации, но в запросе нет кредов. Похоже, что мы хотим сказать: «401: Не авторизован, кредов нет, вы не пройдете». Итак, какой самый простой способ сделать это в resolver? Я полагаю, что следующее:
// get an auth token somehow
const authToken string | null = req.headers.get("Authorization")
assert(
authToken,
"401: There are no creds, you shall not pass"
)
Пользовательский assert
Но это единственная строка, так как мы не можем передать в вызов assert ничего похожего на объект, поэтому давайте напишем свою реализацию assert:
export function assertWithApiError<T>(
statement: T | null | undefined,
errorObject: ApiError
): asserts statement is T {
if (!statement) {
throw newApiError(errorObject)
}
}
Функция assert в Typescript позволяет нам иметь типобезопасные утверждения, в которых мы можем бросать любой бросаемый объект, даже созданный нами самими, с любыми данными, упакованными в ошибку.
// get an auth token somehow
const authToken: string | null = req.headers.get("Authorization")
assertWithApiError(
authToken,
{ message: 'Auth token is not presented', code: 401 }
)
// there is typescript knows that token is a string
Фронтенд
Что насчет фронтенда? Как я уже сказал выше, я использую пакет @svelte/urql для работы с GraphQL в моем приложении svelte. Давайте посмотрим на код!
Начнем с обычного случая использования @svelte/urql:
// mutation function is from @svelte/urql package
const editUserMutation = mutation<{ me: User }>({
query: EDIT_USER,
})
const { data, error } = await editMeMutation({ intro })
Итак, ошибка здесь имеет тип CombinedError
из @svelte/urql, который включает массив исходных ошибок GraphQL:
type OriginalError = {
message: string
stack: string
}
export const parseGraphqlApiErrors = (error: CombinedError): ApiError[] => error.graphQLErrors.map((e) => {
const rawOriginalError = (e.extensions?.originalError as OriginalError).message
return parseApiError(rawOriginalError)
})
export const parseApiError = (jsonString: string): ApiError => {
try {
const parsed: unknown = JSON.parse(jsonString)
if (apiErrorTypeGuard(parsed)) {
return parsed
} else {
throw new Error('got invalid api error')
}
} catch (e) {
console.error(e)
throw Error("Can't parse api error from json string")
}
}
const apiErrorTypeGuard = (possiblyError: any): possiblyError is ApiError =>
typeof possiblyError === 'object' && 'code' in possiblyError && 'message' in possiblyError
В конце истории у нас есть пользовательские утверждения, которые выбрасывают ошибки, наполненные любыми данными, которые мы хотим, и код фронтенда, который может извлечь эти данные; вот и все.
Я не очень опытный пользователь GraphQL, Svelte или Urql, и я буду рад, если вы укажете мне на какое-либо существующее решение, которое лучше, чем описанное выше, и надеюсь, что мои идеи кому-нибудь пригодятся 🙂
Фото Mario Mendez