@@ -11,6 +11,26 @@ type RouteHandler<T = unknown> = (
1111 context : T
1212) => Promise < NextResponse | Response > | NextResponse | Response
1313
14+ /**
15+ * Reads a numeric `statusCode` (4xx or 5xx) off an Error so typed domain errors
16+ * (e.g. `WorkspaceAccessDeniedError`, `InvalidFieldError`) map to the correct
17+ * HTTP status when they bubble up unhandled instead of defaulting to 500.
18+ *
19+ * When a typed status is returned, the error's `message` is sent to the client
20+ * verbatim — matching the NestJS `HttpException` / Spring `ResponseStatusException`
21+ * convention. The safety contract is convention-based: only attach `statusCode`
22+ * to errors whose `message` is safe to expose to clients (no stack traces,
23+ * secrets, file paths, ORM internals). Untyped errors fall back to a generic
24+ * 500 response with no message exposure.
25+ */
26+ function readTypedErrorStatus ( error : unknown ) : number | undefined {
27+ if ( ! ( error instanceof Error ) ) return undefined
28+ const status = ( error as { statusCode ?: unknown } ) . statusCode
29+ if ( typeof status !== 'number' ) return undefined
30+ if ( status < 400 || status >= 600 ) return undefined
31+ return status
32+ }
33+
1434/**
1535 * Wraps a Next.js API route handler with centralized error reporting.
1636 *
@@ -35,8 +55,21 @@ export function withRouteHandler<T>(handler: RouteHandler<T>): RouteHandler<T> {
3555 } catch ( error ) {
3656 const duration = Date . now ( ) - startTime
3757 const message = getErrorMessage ( error , 'Unknown error' )
38- logger . error ( 'Unhandled route error' , { duration, error : message } )
39- response = NextResponse . json ( { error : 'Internal server error' , requestId } , { status : 500 } )
58+ const typedStatus = readTypedErrorStatus ( error )
59+ if ( typedStatus !== undefined ) {
60+ if ( typedStatus >= 500 ) {
61+ logger . error ( 'Unhandled route error' , { duration, status : typedStatus , error : message } )
62+ } else {
63+ logger . warn ( 'Typed route error' , { duration, status : typedStatus , error : message } )
64+ }
65+ response = NextResponse . json ( { error : message , requestId } , { status : typedStatus } )
66+ } else {
67+ logger . error ( 'Unhandled route error' , { duration, error : message } )
68+ response = NextResponse . json (
69+ { error : 'Internal server error' , requestId } ,
70+ { status : 500 }
71+ )
72+ }
4073 response ?. headers ?. set ( 'x-request-id' , requestId )
4174 return response
4275 }
0 commit comments