@@ -11,36 +11,24 @@ type RouteHandler<T = unknown> = (
1111 context : T
1212) => Promise < NextResponse | Response > | NextResponse | Response
1313
14- function defaultMessageForStatus ( status : number ) : string {
15- if ( status >= 500 ) return 'Internal server error'
16- if ( status === 401 ) return 'Unauthorized'
17- if ( status === 403 ) return 'Forbidden'
18- if ( status === 404 ) return 'Not Found'
19- if ( status === 409 ) return 'Conflict'
20- return 'Request failed'
21- }
22-
2314/**
2415 * Reads a numeric `statusCode` (4xx or 5xx) off an Error so typed domain errors
25- * (e.g. `WorkspaceAccessDeniedError`) can map to the correct HTTP status when
26- * they bubble up unhandled instead of defaulting to 500. Returns both the
27- * status and a client-safe message: the error's own `publicMessage` if it
28- * opted in, otherwise a generic per-status string. The raw `error.message` is
29- * never exposed by this fallback — domain errors must explicitly mark their
30- * message as safe to expose, preventing accidental leakage of internal details
31- * from typed errors that didn't intend to be user-facing.
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.
3225 */
33- function readTypedErrorResponse ( error : unknown ) : { status : number ; message : string } | undefined {
26+ function readTypedErrorStatus ( error : unknown ) : number | undefined {
3427 if ( ! ( error instanceof Error ) ) return undefined
35- const typed = error as { statusCode ?: unknown ; publicMessage ?: unknown }
36- const status = typed . statusCode
28+ const status = ( error as { statusCode ?: unknown } ) . statusCode
3729 if ( typeof status !== 'number' ) return undefined
3830 if ( status < 400 || status >= 600 ) return undefined
39- const message =
40- typeof typed . publicMessage === 'string' && typed . publicMessage . length > 0
41- ? typed . publicMessage
42- : defaultMessageForStatus ( status )
43- return { status, message }
31+ return status
4432}
4533
4634/**
@@ -66,28 +54,17 @@ export function withRouteHandler<T>(handler: RouteHandler<T>): RouteHandler<T> {
6654 response = await handler ( request , context )
6755 } catch ( error ) {
6856 const duration = Date . now ( ) - startTime
69- const rawMessage = getErrorMessage ( error , 'Unknown error' )
70- const typed = readTypedErrorResponse ( error )
71- if ( typed !== undefined ) {
72- if ( typed . status >= 500 ) {
73- logger . error ( 'Unhandled route error' , {
74- duration,
75- status : typed . status ,
76- error : rawMessage ,
77- } )
57+ const message = getErrorMessage ( error , 'Unknown error' )
58+ const typedStatus = readTypedErrorStatus ( error )
59+ if ( typedStatus !== undefined ) {
60+ if ( typedStatus >= 500 ) {
61+ logger . error ( 'Unhandled route error' , { duration, status : typedStatus , error : message } )
7862 } else {
79- logger . warn ( 'Typed route error' , {
80- duration,
81- status : typed . status ,
82- error : rawMessage ,
83- } )
63+ logger . warn ( 'Typed route error' , { duration, status : typedStatus , error : message } )
8464 }
85- response = NextResponse . json (
86- { error : typed . message , requestId } ,
87- { status : typed . status }
88- )
65+ response = NextResponse . json ( { error : message , requestId } , { status : typedStatus } )
8966 } else {
90- logger . error ( 'Unhandled route error' , { duration, error : rawMessage } )
67+ logger . error ( 'Unhandled route error' , { duration, error : message } )
9168 response = NextResponse . json (
9269 { error : 'Internal server error' , requestId } ,
9370 { status : 500 }
0 commit comments