graphql-yoga
Version:
199 lines (198 loc) • 6.75 kB
JavaScript
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));
}