Skip to content

Commit 7832929

Browse files
Handle Prisma known request errors structurally
Co-authored-by: Neil Raina <makeitraina@users.noreply.github.com>
1 parent f0aa5bc commit 7832929

2 files changed

Lines changed: 69 additions & 9 deletions

File tree

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

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { CopilotApiError, MessagableError, StatusableError } from '@/types/CopilotApiError'
22
import APIError from '@api/core/exceptions/api'
3-
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'
43
import httpStatus from 'http-status'
54
import { NextRequest, NextResponse } from 'next/server'
65
import { ZodError, ZodFormattedError } from 'zod'
@@ -13,10 +12,45 @@ type ErrorResponse = {
1312
status: number
1413
}
1514

15+
type PrismaKnownRequestError = {
16+
code: string
17+
meta?: Record<string, unknown>
18+
name?: string
19+
}
20+
21+
const PRISMA_KNOWN_REQUEST_ERROR_NAME = 'PrismaClientKnownRequestError'
22+
23+
const getConstructorName = (constructor: unknown) => {
24+
if (typeof constructor === 'function') {
25+
return constructor.name
26+
}
27+
28+
if (!constructor || typeof constructor !== 'object') {
29+
return undefined
30+
}
31+
32+
const name = (constructor as { name?: unknown }).name
33+
return typeof name === 'string' ? name : undefined
34+
}
35+
36+
const isPrismaKnownRequestError = (error: unknown): error is PrismaKnownRequestError => {
37+
if (!error || typeof error !== 'object') {
38+
return false
39+
}
40+
41+
const candidate = error as Partial<PrismaKnownRequestError>
42+
const constructorName = getConstructorName(candidate.constructor)
43+
44+
return (
45+
typeof candidate.code === 'string' &&
46+
(candidate.name === PRISMA_KNOWN_REQUEST_ERROR_NAME || constructorName === PRISMA_KNOWN_REQUEST_ERROR_NAME)
47+
)
48+
}
49+
1650
// P2023 ("Inconsistent column data") covers more than UUIDs (e.g. enum mismatches), so
1751
// only treat it as not-found when the meta message points at a malformed UUID — otherwise
1852
// it stays an unclassified error that logs, rather than being silently hidden as a 404.
19-
const isInvalidUuidError = (error: PrismaClientKnownRequestError) => {
53+
const isInvalidUuidError = (error: PrismaKnownRequestError) => {
2054
if (error.code === 'P2010' && error.meta?.code === '22P02') {
2155
return true
2256
}
@@ -25,14 +59,21 @@ const isInvalidUuidError = (error: PrismaClientKnownRequestError) => {
2559
return error.code === 'P2023' && typeof metaMessage === 'string' && metaMessage.toLowerCase().includes('uuid')
2660
}
2761

28-
const getPrismaKnownRequestErrorResponse = (error: PrismaClientKnownRequestError): ErrorResponse | null => {
62+
const getPrismaKnownRequestErrorResponse = (error: PrismaKnownRequestError): ErrorResponse | null => {
2963
if (error.code === 'P2025' || isInvalidUuidError(error)) {
3064
return {
3165
status: httpStatus.NOT_FOUND,
3266
message: 'The requested resource was not found',
3367
}
3468
}
3569

70+
if (error.code === 'P2002') {
71+
return {
72+
status: httpStatus.CONFLICT,
73+
message: 'A resource with these values already exists',
74+
}
75+
}
76+
3677
return null
3778
}
3879

@@ -68,15 +109,15 @@ const normalizeError = (error: unknown): ErrorResponse => {
68109
}
69110
}
70111

71-
if (error instanceof PrismaClientKnownRequestError) {
112+
if (isPrismaKnownRequestError(error)) {
72113
return getPrismaKnownRequestErrorResponse(error) || defaultResponse
73114
}
74115

75116
return defaultResponse
76117
}
77118

78119
const isExpectedPrismaError = (error: unknown) =>
79-
error instanceof PrismaClientKnownRequestError && getPrismaKnownRequestErrorResponse(error) !== null
120+
isPrismaKnownRequestError(error) && getPrismaKnownRequestErrorResponse(error) !== null
80121

81122
const isExpectedClientError = (error: unknown) => {
82123
return (

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ describe('withErrorHandler util', () => {
133133
expect(console.warn).toHaveBeenCalledWith(error)
134134
})
135135

136-
it('logs unclassified Prisma known request errors', async () => {
136+
it('maps Prisma unique constraint errors to 409 and warns instead of erroring', async () => {
137137
const error = new PrismaClientKnownRequestError('Unique constraint failed', {
138138
code: 'P2002',
139139
clientVersion: '5.19.0',
@@ -144,9 +144,28 @@ describe('withErrorHandler util', () => {
144144

145145
const nextResponse = await withErrorHandler(handler)(req, null)
146146
const response = await nextResponse.json()
147-
expect(response.error).toBe('Something went wrong')
148-
expect(nextResponse.status).toBe(httpStatus.BAD_REQUEST)
149-
expect(console.error).toHaveBeenCalledWith(error)
147+
expect(response.error).toBe('A resource with these values already exists')
148+
expect(nextResponse.status).toBe(httpStatus.CONFLICT)
149+
expect(console.error).not.toHaveBeenCalled()
150+
expect(console.warn).toHaveBeenCalledWith(error)
151+
})
152+
153+
it('recognizes Prisma known request errors across module boundaries', async () => {
154+
const error = {
155+
code: 'P2025',
156+
constructor: { name: 'PrismaClientKnownRequestError' },
157+
message: 'Record not found',
158+
}
159+
const handler = async (_req: NextRequest, _params: any) => {
160+
throw error
161+
}
162+
163+
const nextResponse = await withErrorHandler(handler)(req, null)
164+
const response = await nextResponse.json()
165+
expect(response.error).toBe('The requested resource was not found')
166+
expect(nextResponse.status).toBe(httpStatus.NOT_FOUND)
167+
expect(console.error).not.toHaveBeenCalled()
168+
expect(console.warn).toHaveBeenCalledWith(error)
150169
})
151170

152171
it('logs unexpected errors that default to a 4xx response', async () => {

0 commit comments

Comments
 (0)