@@ -11,17 +11,36 @@ 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+
1423/**
1524 * Reads a numeric `statusCode` (4xx or 5xx) off an Error so typed domain errors
1625 * (e.g. `WorkspaceAccessDeniedError`) can map to the correct HTTP status when
17- * they bubble up unhandled instead of defaulting to 500.
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.
1832 */
19- function readTypedErrorStatus ( error : unknown ) : number | undefined {
33+ function readTypedErrorResponse ( error : unknown ) : { status : number ; message : string } | undefined {
2034 if ( ! ( error instanceof Error ) ) return undefined
21- const status = ( error as { statusCode ?: unknown } ) . statusCode
35+ const typed = error as { statusCode ?: unknown ; publicMessage ?: unknown }
36+ const status = typed . statusCode
2237 if ( typeof status !== 'number' ) return undefined
2338 if ( status < 400 || status >= 600 ) return undefined
24- return status
39+ const message =
40+ typeof typed . publicMessage === 'string' && typed . publicMessage . length > 0
41+ ? typed . publicMessage
42+ : defaultMessageForStatus ( status )
43+ return { status, message }
2544}
2645
2746/**
@@ -47,17 +66,28 @@ export function withRouteHandler<T>(handler: RouteHandler<T>): RouteHandler<T> {
4766 response = await handler ( request , context )
4867 } catch ( error ) {
4968 const duration = Date . now ( ) - startTime
50- const message = getErrorMessage ( error , 'Unknown error' )
51- const typedStatus = readTypedErrorStatus ( error )
52- if ( typedStatus !== undefined ) {
53- if ( typedStatus >= 500 ) {
54- logger . error ( 'Unhandled route error' , { duration, status : typedStatus , error : message } )
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+ } )
5578 } else {
56- logger . warn ( 'Typed route error' , { duration, status : typedStatus , error : message } )
79+ logger . warn ( 'Typed route error' , {
80+ duration,
81+ status : typed . status ,
82+ error : rawMessage ,
83+ } )
5784 }
58- response = NextResponse . json ( { error : message , requestId } , { status : typedStatus } )
85+ response = NextResponse . json (
86+ { error : typed . message , requestId } ,
87+ { status : typed . status }
88+ )
5989 } else {
60- logger . error ( 'Unhandled route error' , { duration, error : message } )
90+ logger . error ( 'Unhandled route error' , { duration, error : rawMessage } )
6191 response = NextResponse . json (
6292 { error : 'Internal server error' , requestId } ,
6393 { status : 500 }
0 commit comments