1dff5bf957
We call convertExceptionToGraphQLError in the exception handler for http exceptions but we don't take into account those that already are graphqlErrors and because of that the logic of convertExceptionToGraphql is to fallback to a 500. Now if the exception is a BaseGraphqlError (custom graphql error we throw in the code), we throw them directly. BEFORE <img width="957" alt="Screenshot 2024-07-12 at 15 33 03" src="https://github.com/user-attachments/assets/22ddae13-4996-4ad3-8f86-dd17c2922ca8"> AFTER <img width="923" alt="Screenshot 2024-07-12 at 15 32 01" src="https://github.com/user-attachments/assets/d3d6db93-6d28-495c-a4b4-ba4e47d45abd"> --------- Co-authored-by: Charles Bochet <charles@twenty.com>
151 lines
4.6 KiB
TypeScript
151 lines
4.6 KiB
TypeScript
import {
|
|
OnExecuteDoneHookResultOnNextHook,
|
|
Plugin,
|
|
getDocumentString,
|
|
handleStreamOrSingleExecutionResult,
|
|
} from '@envelop/core';
|
|
import { GraphQLError, Kind, OperationDefinitionNode, print } from 'graphql';
|
|
|
|
import { GraphQLContext } from 'src/engine/api/graphql/graphql-config/interfaces/graphql-context.interface';
|
|
|
|
import { ExceptionHandlerService } from 'src/engine/integrations/exception-handler/exception-handler.service';
|
|
import {
|
|
convertExceptionToGraphQLError,
|
|
shouldFilterException,
|
|
} from 'src/engine/utils/global-exception-handler.util';
|
|
|
|
export type ExceptionHandlerPluginOptions = {
|
|
/**
|
|
* The exception handler service to use.
|
|
*/
|
|
exceptionHandlerService: ExceptionHandlerService;
|
|
/**
|
|
* The key of the event id in the error's extension. `null` to disable.
|
|
* @default exceptionEventId
|
|
*/
|
|
eventIdKey?: string | null;
|
|
};
|
|
|
|
// This hook is deprecated.
|
|
// We should either handle exception in the context of graphql, controller or command
|
|
// @deprecated
|
|
export const useExceptionHandler = <PluginContext extends GraphQLContext>(
|
|
options: ExceptionHandlerPluginOptions,
|
|
): Plugin<PluginContext> => {
|
|
const eventIdKey = options.eventIdKey === null ? null : 'exceptionEventId';
|
|
|
|
function addEventId(
|
|
err: GraphQLError,
|
|
eventId: string | undefined | null,
|
|
): GraphQLError {
|
|
if (eventIdKey !== null && eventId) {
|
|
err.extensions[eventIdKey] = eventId;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
return {
|
|
async onExecute({ args }) {
|
|
const exceptionHandlerService = options.exceptionHandlerService;
|
|
const rootOperation = args.document.definitions.find(
|
|
(o) => o.kind === Kind.OPERATION_DEFINITION,
|
|
) as OperationDefinitionNode;
|
|
const operationType = rootOperation.operation;
|
|
const user = args.contextValue.req.user;
|
|
const document = getDocumentString(args.document, print);
|
|
const opName =
|
|
args.operationName ||
|
|
rootOperation.name?.value ||
|
|
'Anonymous Operation';
|
|
|
|
return {
|
|
onExecuteDone(payload) {
|
|
const handleResult: OnExecuteDoneHookResultOnNextHook<object> = ({
|
|
result,
|
|
setResult,
|
|
}) => {
|
|
if (result.errors && result.errors.length > 0) {
|
|
const exceptions = result.errors.reduce<{
|
|
filtered: any[];
|
|
unfiltered: any[];
|
|
}>(
|
|
(acc, err) => {
|
|
// Filter out exceptions that we don't want to be captured by exception handler
|
|
if (shouldFilterException(err?.originalError ?? err)) {
|
|
acc.filtered.push(err);
|
|
} else {
|
|
acc.unfiltered.push(err);
|
|
}
|
|
|
|
return acc;
|
|
},
|
|
{
|
|
filtered: [],
|
|
unfiltered: [],
|
|
},
|
|
);
|
|
|
|
if (exceptions.unfiltered.length > 0) {
|
|
const eventIds = exceptionHandlerService.captureExceptions(
|
|
exceptions.unfiltered,
|
|
{
|
|
operation: {
|
|
name: opName,
|
|
type: operationType,
|
|
},
|
|
document,
|
|
user,
|
|
},
|
|
);
|
|
|
|
exceptions.unfiltered.map((err, i) =>
|
|
addEventId(err, eventIds?.[i]),
|
|
);
|
|
}
|
|
|
|
const concatenatedErrors = [
|
|
...exceptions.filtered,
|
|
...exceptions.unfiltered,
|
|
];
|
|
const errors = concatenatedErrors.map((err) => {
|
|
if (!err.originalError) {
|
|
return err;
|
|
}
|
|
|
|
return convertExceptionToGraphQLError(err.originalError);
|
|
});
|
|
|
|
setResult({
|
|
...result,
|
|
errors,
|
|
});
|
|
}
|
|
};
|
|
|
|
return handleStreamOrSingleExecutionResult(payload, handleResult);
|
|
},
|
|
};
|
|
},
|
|
onValidate: ({ context, validateFn, params: { documentAST, schema } }) => {
|
|
const errors = validateFn(schema, documentAST);
|
|
|
|
if (Array.isArray(errors) && errors.length > 0) {
|
|
const headers = context.req.headers;
|
|
const currentSchemaVersion = context.req.cacheVersion;
|
|
|
|
const requestSchemaVersion = headers['x-schema-version'];
|
|
|
|
if (
|
|
requestSchemaVersion &&
|
|
requestSchemaVersion !== currentSchemaVersion
|
|
) {
|
|
throw new GraphQLError(
|
|
`Schema version mismatch, please refresh the page.`,
|
|
);
|
|
}
|
|
}
|
|
},
|
|
};
|
|
};
|