diff --git a/packages/next-api-compose/src/app.ts b/packages/next-api-compose/src/app.ts index c545e9d..5725bcf 100644 --- a/packages/next-api-compose/src/app.ts +++ b/packages/next-api-compose/src/app.ts @@ -1,4 +1,4 @@ -import type { Promisable } from 'type-fest' +import type { PartialDeep, Promisable } from 'type-fest' import type { NextApiRequest, NextApiResponse } from 'next' import type { NextResponse } from 'next/server' @@ -35,6 +35,13 @@ type ComposeParameters< ] > +type ComposeSettings = PartialDeep<{ + sharedErrorHandler: { + handler: (method: NextApiRouteMethod, error: Error) => Promisable + includeRouteHandler: boolean + } +}> + /** * Function that allows to define complex API structure in Next.js App router's Route Handlers. * @@ -51,7 +58,19 @@ export function compose< | Promisable | Promisable > ->(parameters: ComposeParameters) { +>( + parameters: ComposeParameters, + composeSettings?: ComposeSettings +) { + const defaultComposeSettings: ComposeSettings = { + sharedErrorHandler: { + handler: undefined, + includeRouteHandler: false + } + } + + composeSettings = { ...defaultComposeSettings, ...composeSettings } + const modified = Object.entries(parameters).map( ([method, composeForMethodData]: [ UsedMethods, @@ -66,12 +85,49 @@ export function compose< [method]: async (request: any) => { if (typeof composeForMethodData === 'function') { const handler = composeForMethodData + if ( + composeSettings?.sharedErrorHandler?.includeRouteHandler && + composeSettings?.sharedErrorHandler?.handler != null + ) { + try { + return await handler(request) + } catch (error) { + const composeSharedErrorHandlerResult = + await composeSettings?.sharedErrorHandler?.handler(method, error) + + if ( + composeSharedErrorHandlerResult != null && + composeSharedErrorHandlerResult instanceof Response + ) { + return composeSharedErrorHandlerResult + } + } + } return await handler(request) } const [middlewareChain, handler] = composeForMethodData for (const middleware of middlewareChain) { + if (composeSettings?.sharedErrorHandler?.handler != null) { + try { + const abortedMiddleware = await middleware(request) + + if (abortedMiddleware != null && abortedMiddleware instanceof Response) + return abortedMiddleware + } catch (error) { + const composeSharedErrorHandlerResult = + await composeSettings?.sharedErrorHandler?.handler(method, error) + + if ( + composeSharedErrorHandlerResult != null && + composeSharedErrorHandlerResult instanceof Response + ) { + return composeSharedErrorHandlerResult + } + } + } + const abortedMiddleware = await middleware(request) if (abortedMiddleware != null && abortedMiddleware instanceof Response) diff --git a/packages/next-api-compose/test/app.test.ts b/packages/next-api-compose/test/app.test.ts index d2d9798..78c80d9 100644 --- a/packages/next-api-compose/test/app.test.ts +++ b/packages/next-api-compose/test/app.test.ts @@ -88,6 +88,39 @@ describe("composed route handler's http functionality", () => { expect(response.body.foo).toBe('bar') }) + it("should handle errors errors thrown by middlewares and return a 500 response with the error's message", async () => { + function errorMiddleware() { + throw new Error('foo') + } + + const { GET } = compose( + { + GET: [ + [errorMiddleware], + () => { + return new MockedResponse({ foo: 'bar' }) + } + ] + }, + { + sharedErrorHandler: { + handler: (method, error) => { + return new MockedResponse( + { error: error.message }, + 500 + ) as unknown as Response + } + } + } + ) + + const app = createTestServer(GET) + const response = await request(app).get('/') + + expect(response.status).toBe(500) + expect(response.body.error).toBe('foo') + }) + it('should wait for asynchronous middlewares to resolve before moving to the next middleware or handler', async () => { async function setFooAsyncMiddleware(request) { await new Promise((resolve) => setTimeout(resolve, 100)) @@ -151,8 +184,8 @@ describe("composed route handler's code features", () => { } }) - expect(composedMethods).toHaveProperty("GET") - expect(composedMethods).toHaveProperty("POST") + expect(composedMethods).toHaveProperty('GET') + expect(composedMethods).toHaveProperty('POST') }) })