diff --git a/.changeset/hot-toys-grab.md b/.changeset/hot-toys-grab.md new file mode 100644 index 0000000000..a2d4faa469 --- /dev/null +++ b/.changeset/hot-toys-grab.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Add possibility to use custom error handler diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index b80ec84329..d20f568e98 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -521,6 +521,8 @@ export type ServiceBuilder = { setRequestLoggingHandler( requestLoggingHandler: RequestLoggingHandlerFactory, ): ServiceBuilder; + setErrorHandler(errorHandler: ErrorRequestHandler): ServiceBuilder; + disableDefaultErrorHandler(): ServiceBuilder; start(): Promise; }; diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts index bd97f444de..6229cbb683 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { applyCspDirectives } from './ServiceBuilderImpl'; +import { NextFunction, Request, Response } from 'express'; +import { applyCspDirectives, ServiceBuilderImpl } from './ServiceBuilderImpl'; describe('ServiceBuilderImpl', () => { describe('applyCspDirectives', () => { @@ -33,4 +34,27 @@ describe('ServiceBuilderImpl', () => { expect(result!['upgrade-insecure-requests']).toBeUndefined(); }); }); + + describe('setCustomErrorHandler', () => { + it('check if custom error handler is undefined', () => { + const serviceBuilder = new ServiceBuilderImpl(module); + const serviceBuilderProto = Object.getPrototypeOf(serviceBuilder); + expect(serviceBuilderProto.errorHandler).toBeUndefined(); + }); + + it('adds custom error handler', () => { + const serviceBuilder = new ServiceBuilderImpl(module); + const serviceBuilderProto = Object.getPrototypeOf(serviceBuilder); + const customErrorHandler = ( + error: Error, + _req: Request, + _res: Response, + next: NextFunction, + ) => { + next(error); + }; + serviceBuilderProto.setErrorHandler(customErrorHandler); + expect(serviceBuilderProto.errorHandler).toEqual(customErrorHandler); + }); + }); }); diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index b085cccfd6..54f145539f 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -17,7 +17,7 @@ import { Config } from '@backstage/config'; import compression from 'compression'; import cors from 'cors'; -import express, { Router } from 'express'; +import express, { Router, ErrorRequestHandler } from 'express'; import helmet from 'helmet'; import * as http from 'http'; import stoppable from 'stoppable'; @@ -25,7 +25,7 @@ import { Logger } from 'winston'; import { useHotCleanup } from '../../hot'; import { getRootLogger } from '../../logging'; import { - errorHandler, + errorHandler as defaultErrorHandler, notFoundHandler, requestLoggingHandler as defaultRequestLoggingHandler, } from '../../middleware'; @@ -66,6 +66,8 @@ export class ServiceBuilderImpl implements ServiceBuilder { private httpsSettings: HttpsSettings | undefined; private routers: [string, Router][]; private requestLoggingHandler: RequestLoggingHandlerFactory | undefined; + private errorHandler: ErrorRequestHandler | undefined; + private useDefaultErrorHandler: boolean; // Reference to the module where builder is created - needed for hot module // reloading private module: NodeModule; @@ -73,6 +75,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { constructor(moduleRef: NodeModule) { this.routers = []; this.module = moduleRef; + this.useDefaultErrorHandler = true; } loadConfig(config: Config): ServiceBuilder { @@ -152,6 +155,16 @@ export class ServiceBuilderImpl implements ServiceBuilder { return this; } + setErrorHandler(errorHandler: ErrorRequestHandler) { + this.errorHandler = errorHandler; + return this; + } + + disableDefaultErrorHandler() { + this.useDefaultErrorHandler = false; + return this; + } + async start(): Promise { const app = express(); const { port, host, logger, corsOptions, httpsSettings, helmetOptions } = @@ -169,7 +182,14 @@ export class ServiceBuilderImpl implements ServiceBuilder { app.use(root, route); } app.use(notFoundHandler()); - app.use(errorHandler()); + + if (this.errorHandler) { + app.use(this.errorHandler); + } + + if (this.useDefaultErrorHandler) { + app.use(defaultErrorHandler()); + } const server: http.Server = httpsSettings ? await createHttpsServer(app, httpsSettings, logger) diff --git a/packages/backend-common/src/service/types.ts b/packages/backend-common/src/service/types.ts index 2ad379f31e..3e94196006 100644 --- a/packages/backend-common/src/service/types.ts +++ b/packages/backend-common/src/service/types.ts @@ -16,7 +16,7 @@ import { Config } from '@backstage/config'; import cors from 'cors'; -import { Router, RequestHandler } from 'express'; +import { Router, RequestHandler, ErrorRequestHandler } from 'express'; import { Server } from 'http'; import { Logger } from 'winston'; @@ -98,6 +98,21 @@ export type ServiceBuilder = { requestLoggingHandler: RequestLoggingHandlerFactory, ): ServiceBuilder; + /** + * Sets an additional errorHandler to run before the defaultErrorHandler. + * + * For execution of only the custom error handler make sure to also invoke disableDefaultErrorHandler() + * otherwise the defaultErrorHandler is executed at the end of the error middleware chain. + * + * @param errorHandler - an error handler + */ + setErrorHandler(errorHandler: ErrorRequestHandler): ServiceBuilder; + + /** + * Disables the default error handler + */ + disableDefaultErrorHandler(): ServiceBuilder; + /** * Starts the server using the given settings. */