فهرست منبع

feat(core): Introduce ErrorHandlerStrategy

This new strategy enables a unified, global way of responding to errors across the application,
including server and worker.
Michael Bromley 2 سال پیش
والد
کامیت
066e524709

+ 5 - 0
docs/docs/guides/developer-guide/the-api-layer/index.mdx

@@ -170,6 +170,11 @@ import { MyCustomGuard, MyCustomInterceptor, MyCustomExceptionFilter } from './m
             useClass: MyCustomInterceptor,
         },
         {
+            // Note: registering a global "catch all" exception filter
+            // must be used with caution as it will override the built-in
+            // Vendure exception filter. See https://github.com/nestjs/nest/issues/3252
+            // To implement custom error handling, it is recommended to use
+            // a custom ErrorHandlerStrategy instead.
             provide: APP_FILTER,
             useClass: MyCustomExceptionFilter,
         },

+ 9 - 3
packages/core/src/api/middleware/exception-logger.filter.ts

@@ -1,6 +1,6 @@
-import { ArgumentsHost, ExceptionFilter, HttpException } from '@nestjs/common';
+import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
 
-import { Logger, LogLevel } from '../../config';
+import { ConfigService, Logger, LogLevel } from '../../config';
 import { HEALTH_CHECK_ROUTE } from '../../health-check/constants';
 import { I18nError } from '../../i18n/i18n-error';
 import { parseContext } from '../common/parse-context';
@@ -8,8 +8,14 @@ import { parseContext } from '../common/parse-context';
 /**
  * Logs thrown I18nErrors via the configured VendureLogger.
  */
+@Catch()
 export class ExceptionLoggerFilter implements ExceptionFilter {
-    catch(exception: Error | HttpException | I18nError, host: ArgumentsHost) {
+    constructor(private configService: ConfigService) {}
+
+    catch(exception: Error, host: ArgumentsHost) {
+        for (const handler of this.configService.systemOptions.errorHandlers) {
+            void handler.handleServerError(exception, { host });
+        }
         const { req, res, info, isGraphQL } = parseContext(host);
         let message = '';
         let statusCode = 500;

+ 2 - 1
packages/core/src/config/config.module.ts

@@ -103,7 +103,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
         const { customPaymentProcess, process: paymentProcess } = this.configService.paymentOptions;
         const { entityIdStrategy: entityIdStrategyDeprecated } = this.configService;
         const { entityIdStrategy } = this.configService.entityOptions;
-        const { healthChecks } = this.configService.systemOptions;
+        const { healthChecks, errorHandlers } = this.configService.systemOptions;
         const { assetImportStrategy } = this.configService.importExportOptions;
         return [
             ...adminAuthenticationStrategy,
@@ -134,6 +134,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
             stockAllocationStrategy,
             stockDisplayStrategy,
             ...healthChecks,
+            ...errorHandlers,
             assetImportStrategy,
             changedPriceHandlingStrategy,
             ...(Array.isArray(activeOrderStrategy) ? activeOrderStrategy : [activeOrderStrategy]),

+ 1 - 0
packages/core/src/config/default-config.ts

@@ -211,5 +211,6 @@ export const defaultConfig: RuntimeVendureConfig = {
     plugins: [],
     systemOptions: {
         healthChecks: [new TypeORMHealthCheckStrategy()],
+        errorHandlers: [],
     },
 };

+ 1 - 0
packages/core/src/config/index.ts

@@ -78,6 +78,7 @@ export * from './shipping-method/shipping-calculator';
 export * from './shipping-method/shipping-eligibility-checker';
 export * from './shipping-method/shipping-line-assignment-strategy';
 export * from './system/health-check-strategy';
+export * from './system/error-handler-strategy';
 export * from './tax/default-tax-line-calculation-strategy';
 export * from './tax/default-tax-zone-strategy';
 export * from './tax/tax-line-calculation-strategy';

+ 74 - 0
packages/core/src/config/system/error-handler-strategy.ts

@@ -0,0 +1,74 @@
+import { ArgumentsHost } from '@nestjs/common';
+
+import { InjectableStrategy } from '../../common/index';
+import { Job } from '../../job-queue/index';
+
+/**
+ * @description
+ * This strategy defines logic for handling errors thrown during on both the server
+ * and the worker. It can be used for additional logging & monitoring, or for sending error
+ * reports to external services.
+ *
+ * :::info
+ *
+ * This is configured via the `systemOptions.errorHandlers` property of
+ * your VendureConfig.
+ *
+ * :::
+ *
+ * @example
+ * ```ts
+ * import { ArgumentsHost, ExecutionContext } from '\@nestjs/common';
+ * import { GqlContextType, GqlExecutionContext } from '\@nestjs/graphql';
+ * import { ErrorHandlerStrategy, I18nError, Injector, Job, LogLevel } from '\@vendure/core';
+ *
+ * import { MonitoringService } from './monitoring.service';
+ *
+ * export class CustomErrorHandlerStrategy implements ErrorHandlerStrategy {
+ *     private monitoringService: MonitoringService;
+ *
+ *     init(injector: Injector) {
+ *         this.monitoringService = injector.get(MonitoringService);
+ *     }
+ *
+ *     handleServerError(error: Error, { host }: { host: ArgumentsHost }) {
+ *          const errorContext: any = {};
+ *          if (host?.getType<GqlContextType>() === 'graphql') {
+ *              const gqlContext = GqlExecutionContext.create(host as ExecutionContext);
+ *              const info = gqlContext.getInfo();
+ *              errorContext.graphQlInfo = {
+ *                  fieldName: info.fieldName,
+ *                  path: info.path,
+ *              };
+ *          }
+ *          this.monitoringService.captureException(error, errorContext);
+ *     }
+ *
+ *     handleWorkerError(error: Error, { job }: { job: Job }) {
+ *         const errorContext = {
+ *             queueName: job.queueName,
+ *             jobId: job.id,
+ *         };
+ *         this.monitoringService.captureException(error, errorContext);
+ *     }
+ * }
+ * ```
+ *
+ * @since 2.2.0
+ * @docsCategory Errors
+ */
+export interface ErrorHandlerStrategy extends InjectableStrategy {
+    /**
+     * @description
+     * This method will be invoked for any error thrown during the execution of the
+     * server.
+     */
+    handleServerError(exception: Error, context: { host: ArgumentsHost }): void | Promise<void>;
+
+    /**
+     * @description
+     * This method will be invoked for any error thrown during the execution of a
+     * job on the worker.
+     */
+    handleWorkerError(exception: Error, context: { job: Job }): void | Promise<void>;
+}

+ 10 - 0
packages/core/src/config/vendure-config.ts

@@ -50,6 +50,7 @@ import { SessionCacheStrategy } from './session-cache/session-cache-strategy';
 import { ShippingCalculator } from './shipping-method/shipping-calculator';
 import { ShippingEligibilityChecker } from './shipping-method/shipping-eligibility-checker';
 import { ShippingLineAssignmentStrategy } from './shipping-method/shipping-line-assignment-strategy';
+import { ErrorHandlerStrategy } from './system/error-handler-strategy';
 import { HealthCheckStrategy } from './system/health-check-strategy';
 import { TaxLineCalculationStrategy } from './tax/tax-line-calculation-strategy';
 import { TaxZoneStrategy } from './tax/tax-zone-strategy';
@@ -1019,6 +1020,15 @@ export interface SystemOptions {
      * @since 1.6.0
      */
     healthChecks?: HealthCheckStrategy[];
+    /**
+     * @description
+     * Defines an array of {@link ErrorHandlerStrategy} instances which are used to define logic to be executed
+     * when an error occurs, either on the server or the worker.
+     *
+     * @default []
+     * @since 2.2.0
+     */
+    errorHandlers?: ErrorHandlerStrategy[];
 }
 
 /**

+ 24 - 0
packages/core/src/job-queue/job-queue.service.ts

@@ -72,6 +72,8 @@ export class JobQueueService implements OnModuleDestroy {
         if (this.configService.jobQueueOptions.prefix) {
             options = { ...options, name: `${this.configService.jobQueueOptions.prefix}${options.name}` };
         }
+        const wrappedProcessFn = this.createWrappedProcessFn(options.process);
+        options = { ...options, process: wrappedProcessFn };
         const queue = new JobQueue(options, this.jobQueueStrategy, this.jobBufferService);
         if (this.hasStarted && this.shouldStartQueue(queue.name)) {
             await queue.start();
@@ -164,6 +166,28 @@ export class JobQueueService implements OnModuleDestroy {
         }));
     }
 
+    /**
+     * We wrap the process function in order to catch any errors thrown and pass them to
+     * any configured ErrorHandlerStrategies.
+     */
+    private createWrappedProcessFn<Data extends JobData<Data>>(
+        processFn: (job: Job<Data>) => Promise<any>,
+    ): (job: Job<Data>) => Promise<any> {
+        const { errorHandlers } = this.configService.systemOptions;
+        return async (job: Job<Data>) => {
+            try {
+                return await processFn(job);
+            } catch (e) {
+                for (const handler of errorHandlers) {
+                    if (e instanceof Error) {
+                        void handler.handleWorkerError(e, { job });
+                    }
+                }
+                throw e;
+            }
+        };
+    }
+
     private shouldStartQueue(queueName: string): boolean {
         if (this.configService.jobQueueOptions.activeQueues.length > 0) {
             if (!this.configService.jobQueueOptions.activeQueues.includes(queueName)) {