UNPKG

graphql-yoga

Version:
199 lines (198 loc) • 6.75 kB
import { GraphQLError } from 'graphql'; import { InvalidContentLengthError, RequestBodyTooLargeError, } from '@whatwg-node/server'; /** * Attach experimental schema `coordinate` to a GraphQLError. * graphql-js GraphQLErrorOptions does not know about coordinate yet, so this * uses defineProperty (same approach as `@graphql-tools/utils` locatedError). * * Prefer reading with `getSchemaCoordinate` from `@graphql-tools/utils`. */ export function withSchemaCoordinate(error, coordinate) { if (coordinate != null && error.coordinate == null) { Object.defineProperties(error, { coordinate: { value: coordinate, enumerable: true, configurable: true }, }); } return error; } function isAggregateError(obj) { return obj != null && typeof obj === 'object' && 'errors' in obj; } function hasToString(obj) { return obj != null && typeof obj.toString === 'function'; } export function isGraphQLError(val) { return val instanceof GraphQLError; } export function isOriginalGraphQLError(val) { if (val instanceof GraphQLError) { if (val.originalError != null) { return isOriginalGraphQLError(val.originalError); } return true; } return false; } export function isAbortError(error) { return (typeof error === 'object' && error?.constructor?.name === 'DOMException' && (error.name === 'AbortError' || error.name === 'TimeoutError')); } export function isRequestBodyLimitError(error) { return error instanceof RequestBodyTooLargeError || error instanceof InvalidContentLengthError; } function headersInitToRecord(headers) { return Object.fromEntries(new Headers(headers).entries()); } function graphQLErrorFromBodyLimitError(error) { const headers = headersInitToRecord(error.headers); if (error instanceof RequestBodyTooLargeError) { return new GraphQLError(error.message, { originalError: error, extensions: { http: { status: 413, ...(Object.keys(headers).length > 0 ? { headers } : {}), }, code: 'REQUEST_ENTITY_TOO_LARGE', }, }); } return new GraphQLError(error.message, { originalError: error, extensions: { http: { status: 400, ...(Object.keys(headers).length > 0 ? { headers } : {}), }, code: 'BAD_REQUEST', }, }); } // Passed as `responseFromError` to `@whatwg-node/server`'s `useLimitRequestBodySize` so the // early (Content-Length based) rejection responds with a GraphQL-shaped error body instead of // that plugin's plain-text default. export function responseFromBodyLimitError(error, fetchAPI) { if (!isRequestBodyLimitError(error)) { throw error; } return new fetchAPI.Response(JSON.stringify({ errors: [graphQLErrorFromBodyLimitError(error)] }), { status: error.status, headers: { 'content-type': 'application/json; charset=utf-8', ...error.headers, }, }); } export function handleError(error, maskedErrorsOpts, context) { const log = context.log; const errors = new Set(); if (isAggregateError(error)) { for (const singleError of error.errors) { const handledErrors = handleError(singleError, maskedErrorsOpts, context); for (const handledError of handledErrors) { errors.add(handledError); } } } else if (isAbortError(error)) { log.debug('Request aborted'); } else if (isRequestBodyLimitError(error)) { errors.add(graphQLErrorFromBodyLimitError(error)); } else if (maskedErrorsOpts) { const maskedError = maskedErrorsOpts.maskError(error, maskedErrorsOpts.errorMessage, maskedErrorsOpts.isDev, context); errors.add(isGraphQLError(maskedError) ? maskedError : new GraphQLError(maskedError.message, { originalError: maskedError, })); } else if (isGraphQLError(error)) { errors.add(error); } else if (error instanceof Error) { errors.add(new GraphQLError(error.message, { originalError: error, })); } else if (typeof error === 'string') { errors.add(new GraphQLError(error, { extensions: { code: 'INTERNAL_SERVER_ERROR', unexpected: true, }, })); } else if (hasToString(error)) { errors.add(new GraphQLError(error.toString(), { extensions: { code: 'INTERNAL_SERVER_ERROR', unexpected: true, }, })); } else { log.error({ err: error }); errors.add(new GraphQLError('Unexpected error.', { extensions: { unexpected: true, }, })); } return Array.from(errors); } export function getResponseInitByRespectingErrors(result, headers = {}, isApplicationJson = false) { let status; let unexpectedErrorExists = false; if ('extensions' in result && result.extensions?.http) { if (result.extensions.http.headers) { Object.assign(headers, result.extensions.http.headers); } if (result.extensions.http.status) { status = result.extensions.http.status; } } if ('errors' in result && result.errors?.length) { for (const error of result.errors) { if (error.extensions?.['http']) { if (error.extensions['http'].headers) { Object.assign(headers, error.extensions['http'].headers); } if (isApplicationJson && error.extensions['http'].spec) { continue; } if (error.extensions['http'].status && (!status || error.extensions['http'].status > status)) { status = error.extensions['http'].status; } } else if (!isOriginalGraphQLError(error) || error.extensions?.['unexpected']) { unexpectedErrorExists = true; } } } else { status ||= 200; } if (!status) { if (unexpectedErrorExists && !('data' in result)) { status = 500; } else { status = 200; } } return { status, headers, }; } export function areGraphQLErrors(obj) { return (Array.isArray(obj) && obj.length > 0 && // if one item in the array is a GraphQLError, we're good obj.some(isGraphQLError)); }