Skip to content

Commit

Permalink
Add shared error handler
Browse files Browse the repository at this point in the history
  • Loading branch information
neg4n committed Oct 29, 2023
1 parent 012e6f4 commit 78100b7
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 4 deletions.
60 changes: 58 additions & 2 deletions packages/next-api-compose/src/app.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -35,6 +35,13 @@ type ComposeParameters<
]
>

type ComposeSettings = PartialDeep<{
sharedErrorHandler: {
handler: (method: NextApiRouteMethod, error: Error) => Promisable<Response | void>
includeRouteHandler: boolean
}
}>

/**
* Function that allows to define complex API structure in Next.js App router's Route Handlers.
*
Expand All @@ -51,7 +58,19 @@ export function compose<
| Promisable<Response | undefined>
| Promisable<void | undefined>
>
>(parameters: ComposeParameters<UsedMethods, MiddlewareChain>) {
>(
parameters: ComposeParameters<UsedMethods, MiddlewareChain>,
composeSettings?: ComposeSettings
) {
const defaultComposeSettings: ComposeSettings = {
sharedErrorHandler: {
handler: undefined,
includeRouteHandler: false
}
}

composeSettings = { ...defaultComposeSettings, ...composeSettings }

const modified = Object.entries(parameters).map(
([method, composeForMethodData]: [
UsedMethods,
Expand All @@ -66,12 +85,49 @@ export function compose<
[method]: async (request: any) => {
if (typeof composeForMethodData === 'function') {
const handler = composeForMethodData
if (
composeSettings?.sharedErrorHandler?.includeRouteHandler &&

Check warning on line 89 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

Check warning on line 89 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
composeSettings?.sharedErrorHandler?.handler != null

Check warning on line 90 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 90 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
) {
try {

Check warning on line 92 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
return await handler(request)

Check warning on line 93 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
} catch (error) {

Check warning on line 94 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
const composeSharedErrorHandlerResult =

Check warning on line 95 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
await composeSettings?.sharedErrorHandler?.handler(method, error)

Check warning on line 96 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 97 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
if (

Check warning on line 98 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
composeSharedErrorHandlerResult != null &&

Check warning on line 99 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
composeSharedErrorHandlerResult instanceof Response

Check warning on line 100 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
) {

Check warning on line 101 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
return composeSharedErrorHandlerResult

Check warning on line 102 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 103 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 104 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 105 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 105 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
return await handler(request)
}

const [middlewareChain, handler] = composeForMethodData

for (const middleware of middlewareChain) {
if (composeSettings?.sharedErrorHandler?.handler != null) {

Check warning on line 112 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

Check warning on line 112 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
try {
const abortedMiddleware = await middleware(request)

Check warning on line 115 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
if (abortedMiddleware != null && abortedMiddleware instanceof Response)

Check warning on line 116 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
return abortedMiddleware

Check warning on line 117 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
} catch (error) {
const composeSharedErrorHandlerResult =
await composeSettings?.sharedErrorHandler?.handler(method, error)

Check warning on line 120 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

Check warning on line 120 in packages/next-api-compose/src/app.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

if (
composeSharedErrorHandlerResult != null &&
composeSharedErrorHandlerResult instanceof Response
) {
return composeSharedErrorHandlerResult
}
}
}

const abortedMiddleware = await middleware(request)

if (abortedMiddleware != null && abortedMiddleware instanceof Response)
Expand Down
37 changes: 35 additions & 2 deletions packages/next-api-compose/test/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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')
})
})

Expand Down

0 comments on commit 78100b7

Please sign in to comment.