11import { z } from "zod" ;
22
3+ import { sanitizeServerDetail } from "./errors/sanitizeServerDetail" ;
4+ import { formatErrorMessage } from "./utils" ;
5+
36type JSONValue =
47 | string
58 | number
69 | boolean
710 | { [ x : string ] : JSONValue }
8- | JSONValue [ ] ;
11+ | JSONValue [ ]
12+ | null ;
13+
14+ type ApiErrorContext = {
15+ sessionWasUsed ?: boolean ;
16+ } ;
17+
18+ const JSON_MESSAGE_PRIORITY_KEYS = [
19+ "detail" ,
20+ "message" ,
21+ "error" ,
22+ "errors" ,
23+ "non_field_errors" ,
24+ "root" ,
25+ "__all__" ,
26+ "title" ,
27+ ] ;
928
1029export function isApiError ( e : Error | ApiError | unknown ) : e is ApiError {
1130 return e instanceof ApiError ;
@@ -14,69 +33,173 @@ export function isApiError(e: Error | ApiError | unknown): e is ApiError {
1433export class ApiError extends Error {
1534 response : Response ;
1635 responseJson ?: JSONValue ;
36+ responseText ?: string ;
37+ context ?: ApiErrorContext ;
38+ responseSummary ?: string ;
1739
1840 constructor ( args : {
1941 message : string ;
2042 response : Response ;
2143 responseJson ?: JSONValue ;
44+ responseText ?: string ;
45+ context ?: ApiErrorContext ;
46+ responseSummary ?: string ;
2247 } ) {
2348 super ( args . message ) ;
2449 this . responseJson = args . responseJson ;
2550 this . response = args . response ;
51+ this . responseText = args . responseText ;
52+ this . context = args . context ;
53+ this . responseSummary = args . responseSummary ;
2654 }
2755
28- static async createFromResponse ( response : Response ) : Promise < ApiError > {
56+ static async createFromResponse (
57+ response : Response ,
58+ context : ApiErrorContext = { }
59+ ) : Promise < ApiError > {
60+ let responseText : string | undefined ;
2961 let responseJson : JSONValue | undefined ;
3062 try {
31- responseJson = await response . json ( ) ;
63+ responseText = await response . clone ( ) . text ( ) ;
64+ } catch {
65+ responseText = undefined ;
66+ }
67+ try {
68+ responseJson = responseText
69+ ? JSON . parse ( responseText )
70+ : await response . json ( ) ;
3271 // eslint-disable-next-line @typescript-eslint/no-unused-vars
3372 } catch ( e ) {
3473 responseJson = undefined ;
3574 }
3675
76+ const responseSummary = extractMessage ( responseJson ?? responseText ) ;
77+
3778 return new ApiError ( {
38- message : ` ${ response . status } : ${ response . statusText } ` ,
79+ message : buildApiErrorMessage ( { response, responseJson } ) ,
3980 response : response ,
4081 responseJson : responseJson ,
82+ responseText,
83+ context,
84+ responseSummary,
4185 } ) ;
4286 }
4387
4488 getFieldErrors ( ) : { [ key : string | "root" ] : string [ ] } {
45- if ( typeof this . responseJson !== "object" )
89+ if ( ! this . responseJson || typeof this . responseJson !== "object" )
4690 return { root : [ "Unknown error occurred" ] } ;
4791 if ( Array . isArray ( this . responseJson ) ) {
4892 return {
49- root : this . responseJson . map ( ( x ) => x . toString ( ) ) ,
93+ root : this . responseJson . map ( ( x ) => String ( x ) ) ,
5094 } ;
5195 } else {
5296 return Object . fromEntries (
5397 Object . entries ( this . responseJson ) . map ( ( [ key , val ] ) => {
5498 return [
5599 key ,
56- Array . isArray ( val )
57- ? val . map ( ( x ) => x . toString ( ) )
58- : [ val . toString ( ) ] ,
100+ Array . isArray ( val ) ? val . map ( ( x ) => String ( x ) ) : [ String ( val ) ] ,
59101 ] ;
60102 } )
61103 ) ;
62104 }
63105 }
106+
107+ get statusCode ( ) : number {
108+ return this . response . status ;
109+ }
110+
111+ get statusText ( ) : string {
112+ return this . response . statusText ;
113+ }
64114}
65115
116+ /**
117+ * Raised when a request body fails validation against its Zod schema.
118+ */
66119export class RequestBodyParseError extends Error {
67120 constructor ( public error : z . ZodError ) {
68- super ( error . message ) ;
121+ super ( formatErrorMessage ( error ) ) ;
69122 }
70123}
71124
125+ /**
126+ * Raised when query parameters fail validation against their Zod schema.
127+ */
72128export class RequestQueryParamsParseError extends Error {
73129 constructor ( public error : z . ZodError ) {
74- super ( error . message ) ;
130+ super ( formatErrorMessage ( error ) ) ;
75131 }
76132}
77133
134+ /**
135+ * Raised when response payloads fail validation against their expected Zod schema.
136+ */
78137export class ParseError extends Error {
79138 constructor ( public error : z . ZodError ) {
80- super ( error . message ) ;
139+ super ( formatErrorMessage ( error ) ) ;
140+ }
141+ }
142+
143+ function buildApiErrorMessage ( args : {
144+ response : Response ;
145+ responseJson ?: JSONValue ;
146+ } ) : string {
147+ const { response, responseJson } = args ;
148+ const statusLabel = getStatusLabel ( response ) ;
149+ const detailMessage = extractMessage ( responseJson ) ;
150+
151+ if ( detailMessage ) {
152+ return `${ detailMessage } (${ statusLabel } )` ;
153+ }
154+
155+ return statusLabel ;
156+ }
157+
158+ function extractMessage ( value : JSONValue | undefined ) : string | undefined {
159+ if ( value === undefined || value === null ) return undefined ;
160+
161+ if ( typeof value === "string" ) {
162+ return sanitizeServerDetail ( value ) ;
163+ }
164+
165+ if ( typeof value === "number" || typeof value === "boolean" ) {
166+ return sanitizeServerDetail ( String ( value ) ) ;
167+ }
168+
169+ if ( Array . isArray ( value ) ) {
170+ const parts = value
171+ . map ( ( item ) => extractMessage ( item ) )
172+ . filter ( ( item ) : item is string => Boolean ( item ) ) ;
173+
174+ if ( parts . length > 0 ) {
175+ return sanitizeServerDetail ( parts . join ( " " ) ) ;
176+ }
177+
178+ return undefined ;
81179 }
180+
181+ const objectValue = value as { [ x : string ] : JSONValue } ;
182+
183+ for ( const key of JSON_MESSAGE_PRIORITY_KEYS ) {
184+ if ( key in objectValue ) {
185+ const message = extractMessage ( objectValue [ key ] ) ;
186+ if ( message ) {
187+ return sanitizeServerDetail ( message ) ;
188+ }
189+ }
190+ }
191+
192+ for ( const entry of Object . values ( objectValue ) ) {
193+ const message = extractMessage ( entry ) ;
194+ if ( message ) {
195+ return message ;
196+ }
197+ }
198+
199+ return undefined ;
200+ }
201+
202+ function getStatusLabel ( response : Response ) : string {
203+ const statusText = response . statusText ?. trim ( ) || "Error" ;
204+ return `${ response . status } ${ statusText } ` . trim ( ) ;
82205}
0 commit comments