Skip to content

Commit 3692523

Browse files
Handle expected Prisma API errors quietly
Co-authored-by: Neil Raina <makeitraina@users.noreply.github.com>
1 parent 2e911d4 commit 3692523

2 files changed

Lines changed: 140 additions & 34 deletions

File tree

src/app/api/core/utils/withErrorHandler.ts

Lines changed: 74 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,77 @@ import { ZodError, ZodFormattedError } from 'zod'
77

88
export type RequestHandler = (req: NextRequest, params: any) => Promise<NextResponse>
99

10+
type ErrorResponse = {
11+
status: number
12+
message: string | ZodFormattedError<string>
13+
errors?: unknown[]
14+
shouldLog: boolean
15+
}
16+
17+
const getZodErrorMessage = (error: ZodError) => {
18+
const flattened = error.flatten()
19+
const allMessages = [...flattened.formErrors, ...Object.values(flattened.fieldErrors).flat()].filter(Boolean)
20+
21+
return allMessages[0] || (error.format() as ZodFormattedError<string>)
22+
}
23+
24+
const shouldLogStatus = (status: number) => status >= httpStatus.INTERNAL_SERVER_ERROR
25+
26+
const getPrismaErrorResponse = (error: PrismaClientKnownRequestError): ErrorResponse => {
27+
if (error.code === 'P2025' || error.code === 'P2023') {
28+
return {
29+
status: httpStatus.NOT_FOUND,
30+
message: 'The requested resource was not found',
31+
shouldLog: false,
32+
}
33+
}
34+
35+
return {
36+
status: httpStatus.BAD_REQUEST,
37+
message: 'Something went wrong',
38+
shouldLog: true,
39+
}
40+
}
41+
42+
const getErrorResponse = (error: unknown): ErrorResponse => {
43+
if (error instanceof ZodError) {
44+
return {
45+
status: httpStatus.UNPROCESSABLE_ENTITY,
46+
message: getZodErrorMessage(error),
47+
shouldLog: false,
48+
}
49+
}
50+
51+
if (error instanceof CopilotApiError) {
52+
const status = error.status || httpStatus.BAD_REQUEST
53+
54+
return {
55+
status,
56+
message: error.body.message || 'Something went wrong',
57+
shouldLog: shouldLogStatus(status),
58+
}
59+
}
60+
61+
if (error instanceof APIError) {
62+
return {
63+
status: error.status,
64+
message: error.message || 'Something went wrong',
65+
errors: error.errors,
66+
shouldLog: shouldLogStatus(error.status),
67+
}
68+
}
69+
70+
if (error instanceof PrismaClientKnownRequestError) {
71+
return getPrismaErrorResponse(error)
72+
}
73+
74+
return {
75+
status: (error as StatusableError).status || httpStatus.BAD_REQUEST,
76+
message: (error as MessagableError).body?.message || 'Something went wrong',
77+
shouldLog: true,
78+
}
79+
}
80+
1081
/**
1182
* Reusable utility that wraps a given request handler with a global error handler to standardize response structure
1283
* in case of failures. Catches exceptions thrown from the handler, and returns a formatted error response.
@@ -27,42 +98,13 @@ export type RequestHandler = (req: NextRequest, params: any) => Promise<NextResp
2798
*/
2899
export const withErrorHandler = (handler: RequestHandler): RequestHandler => {
29100
return async (req: NextRequest, params: any) => {
30-
// Execute the handler wrapped in a try... catch block
31101
try {
32102
return await handler(req, params)
33103
} catch (error: unknown) {
34-
// Format error in a readable way
104+
const { status, message, errors, shouldLog } = getErrorResponse(error)
35105

36-
let formattedError = error
37-
if (error instanceof ZodError) {
38-
formattedError = error.format() as ZodFormattedError<string>
39-
}
40-
console.error(formattedError)
41-
42-
// Default staus and message for JSON error response
43-
let status: number = (error as StatusableError).status || httpStatus.BAD_REQUEST
44-
let message: string | ZodFormattedError<string> = (error as MessagableError).body?.message || 'Something went wrong'
45-
let errors: unknown[] | undefined = undefined
46-
47-
// Build a proper response based on the type of Error encountered
48-
if (error instanceof ZodError) {
49-
status = httpStatus.UNPROCESSABLE_ENTITY
50-
const flattened = error.flatten()
51-
const allMessages = [...flattened.formErrors, ...Object.values(flattened.fieldErrors).flat()].filter(Boolean)
52-
message = allMessages[0] || (formattedError as ZodFormattedError<string>)
53-
} else if (error instanceof CopilotApiError) {
54-
status = error.status || status
55-
message = error.body.message || message
56-
} else if (error instanceof APIError) {
57-
status = error.status
58-
message = error.message || message
59-
errors = error.errors
60-
} else if (error instanceof PrismaClientKnownRequestError) {
61-
if (error.code === 'P2025') {
62-
// Code for NOT FOUND in Prisma
63-
status = httpStatus.NOT_FOUND
64-
message = 'The requested resource was not found'
65-
}
106+
if (shouldLog) {
107+
console.error(error instanceof ZodError ? error.format() : error)
66108
}
67109

68110
return NextResponse.json({ error: message, errors }, { status })

src/app/api/tests/utils/withErrorHandler.test.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import APIError from '@api/core/exceptions/api'
55
import { NextRequest, NextResponse } from 'next/server'
66
import { withErrorHandler } from '@api/core/utils/withErrorHandler'
77
import { CopilotApiError } from '@/types/CopilotApiError'
8+
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'
89
import { z } from 'zod'
910

1011
jest.mock('@/utils/CopilotAPI', () => ({
@@ -13,10 +14,21 @@ jest.mock('@/utils/CopilotAPI', () => ({
1314

1415
describe('withErrorHandler util', () => {
1516
let req: NextRequest
17+
const buildPrismaError = (code: string) =>
18+
new PrismaClientKnownRequestError('Prisma known request error', {
19+
code,
20+
clientVersion: 'test',
21+
meta: {},
22+
})
1623

1724
beforeEach(() => {
1825
jest.clearAllMocks()
1926
req = buildNextRequest(`/?token=iu-token`)
27+
jest.spyOn(console, 'error').mockImplementation(() => undefined)
28+
})
29+
30+
afterEach(() => {
31+
jest.restoreAllMocks()
2032
})
2133

2234
it('catches and builds proper response for APIError', async () => {
@@ -28,6 +40,7 @@ describe('withErrorHandler util', () => {
2840
const response = await nextResponse.json()
2941
expect(response.error).toBe('Please provide a valid token')
3042
expect(nextResponse.status).toBe(httpStatus.UNAUTHORIZED)
43+
expect(console.error).not.toHaveBeenCalled()
3144
})
3245

3346
it('catches and builds proper response for ZodError', async () => {
@@ -38,9 +51,9 @@ describe('withErrorHandler util', () => {
3851

3952
const nextResponse = await withErrorHandler(handler)(req, null)
4053
const response = await nextResponse.json()
41-
expect(response.error[0].expected).toBe('string')
42-
expect(response.error[0].received).toBe('number')
54+
expect(response.error).toBe('Expected string, received number')
4355
expect(nextResponse.status).toBe(httpStatus.UNPROCESSABLE_ENTITY)
56+
expect(console.error).not.toHaveBeenCalled()
4457
})
4558

4659
it('catches and builds proper response for CopilotApiError', async () => {
@@ -52,6 +65,57 @@ describe('withErrorHandler util', () => {
5265
const response = await nextResponse.json()
5366
expect(response.error).toBe('Please provide a valid token')
5467
expect(nextResponse.status).toBe(httpStatus.UNAUTHORIZED)
68+
expect(console.error).not.toHaveBeenCalled()
69+
})
70+
71+
it('catches and builds proper response for Prisma not found errors without logging', async () => {
72+
const handler = async (_req: NextRequest, _params: any) => {
73+
throw buildPrismaError('P2025')
74+
}
75+
76+
const nextResponse = await withErrorHandler(handler)(req, null)
77+
const response = await nextResponse.json()
78+
expect(response.error).toBe('The requested resource was not found')
79+
expect(nextResponse.status).toBe(httpStatus.NOT_FOUND)
80+
expect(console.error).not.toHaveBeenCalled()
81+
})
82+
83+
it('catches and builds proper response for invalid Prisma UUID errors without logging', async () => {
84+
const handler = async (_req: NextRequest, _params: any) => {
85+
throw buildPrismaError('P2023')
86+
}
87+
88+
const nextResponse = await withErrorHandler(handler)(req, null)
89+
const response = await nextResponse.json()
90+
expect(response.error).toBe('The requested resource was not found')
91+
expect(nextResponse.status).toBe(httpStatus.NOT_FOUND)
92+
expect(console.error).not.toHaveBeenCalled()
93+
})
94+
95+
it('logs unclassified Prisma known request errors', async () => {
96+
const error = buildPrismaError('P2002')
97+
const handler = async (_req: NextRequest, _params: any) => {
98+
throw error
99+
}
100+
101+
const nextResponse = await withErrorHandler(handler)(req, null)
102+
const response = await nextResponse.json()
103+
expect(response.error).toBe('Something went wrong')
104+
expect(nextResponse.status).toBe(httpStatus.BAD_REQUEST)
105+
expect(console.error).toHaveBeenCalledWith(error)
106+
})
107+
108+
it('logs unexpected errors', async () => {
109+
const error = new Error('boom')
110+
const handler = async (_req: NextRequest, _params: any) => {
111+
throw error
112+
}
113+
114+
const nextResponse = await withErrorHandler(handler)(req, null)
115+
const response = await nextResponse.json()
116+
expect(response.error).toBe('Something went wrong')
117+
expect(nextResponse.status).toBe(httpStatus.BAD_REQUEST)
118+
expect(console.error).toHaveBeenCalledWith(error)
55119
})
56120

57121
it('returns proper response if no errors are encountered', async () => {

0 commit comments

Comments
 (0)