Skip to content

Commit 4b3206d

Browse files
feat: improve client error handing (#383)
* feat: improve clients error handing * feat: improve logic + add tests * refactor: rework a little + improvements * chore: remove useless test
1 parent 8ec9bd5 commit 4b3206d

File tree

5 files changed

+568
-10
lines changed

5 files changed

+568
-10
lines changed

clients/javascript/lib/baseClient.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
NotFoundError,
1313
UnauthorizedError,
1414
UnprocessableEntityError,
15+
sanitizeErrorData,
1516
} from './helpers/errors'
1617

1718
/**
@@ -126,14 +127,20 @@ export abstract class NitteiBaseClient {
126127
// We handle the errors ourselves in the `handleStatusCode` call below
127128
// This is just in case
128129
if (error instanceof AxiosError) {
130+
const sanitizedErrorData = sanitizeErrorData(
131+
error?.response?.data ??
132+
(error.cause as Error)?.message ??
133+
error.message
134+
)
129135
throw new Error(
130-
`Request failed with ${error?.status ? `status code ${error.status}` : 'no status code'} (${error?.response?.data ?? (error.cause as Error)?.message ?? error.toJSON()})`
136+
`Request failed with ${error?.status ? `status code ${error.status}` : 'no status code'} (${sanitizedErrorData})`
131137
)
132138
}
133139
// This might happen if we don't have any status code
134-
throw new Error(
135-
`Unknown error (no status code) (${(error as Error)?.message ?? error})`
140+
const sanitizedErrorData = sanitizeErrorData(
141+
(error as Error)?.message ?? String(error)
136142
)
143+
throw new Error(`Unknown error (no status code) (${sanitizedErrorData})`)
137144
}
138145

139146
if (!res) {
@@ -261,30 +268,33 @@ export abstract class NitteiBaseClient {
261268
*/
262269
private handleStatusCode(res: AxiosResponse): void {
263270
if (res.status >= 500) {
271+
const sanitizedErrorString = sanitizeErrorData(res.data)
264272
throw new Error(
265-
`Internal server error, please try again later (${res.status}) (${res.data})`
273+
`Internal server error, please try again later (${res.status}) (${sanitizedErrorString})`
266274
)
267275
}
268276

269277
if (res.status >= 400) {
278+
const sanitizedErrorData = sanitizeErrorData(res.data)
279+
270280
if (res.status === 400) {
271-
throw new BadRequestError(res.data)
281+
throw new BadRequestError(sanitizedErrorData)
272282
}
273283
if (res.status === 401 || res.status === 403) {
274-
throw new UnauthorizedError(res.data)
284+
throw new UnauthorizedError(sanitizedErrorData)
275285
}
276286
if (res.status === 404) {
277-
throw new NotFoundError(res.data)
287+
throw new NotFoundError(sanitizedErrorData)
278288
}
279289
if (res.status === 409) {
280-
throw new ConflictError(res.data)
290+
throw new ConflictError(sanitizedErrorData)
281291
}
282292
if (res.status === 422) {
283-
throw new UnprocessableEntityError(res.data)
293+
throw new UnprocessableEntityError(sanitizedErrorData)
284294
}
285295

286296
throw new Error(
287-
`Request failed with status code ${res.status} (${res.data})`
297+
`Request failed with status code ${res.status} (${sanitizedErrorData})`
288298
)
289299
}
290300
}

clients/javascript/lib/helpers/errors.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export class BadRequestError extends Error {
88
*/
99
constructor(public apiMessage: string) {
1010
super('Bad request')
11+
this.name = 'BadRequestError'
1112
}
1213
}
1314

@@ -21,6 +22,7 @@ export class NotFoundError extends Error {
2122
*/
2223
constructor(public apiMessage: string) {
2324
super('Not found')
25+
this.name = 'NotFoundError'
2426
}
2527
}
2628

@@ -34,6 +36,7 @@ export class UnauthorizedError extends Error {
3436
*/
3537
constructor(public apiMessage: string) {
3638
super('Unauthorized')
39+
this.name = 'UnauthorizedError'
3740
}
3841
}
3942

@@ -48,6 +51,7 @@ export class ConflictError extends Error {
4851
*/
4952
constructor(public apiMessage: string) {
5053
super('Conflict')
54+
this.name = 'ConflictError'
5155
}
5256
}
5357

@@ -62,5 +66,109 @@ export class UnprocessableEntityError extends Error {
6266
*/
6367
constructor(public apiMessage: string) {
6468
super('Unprocessable entity')
69+
this.name = 'UnprocessableEntityError'
6570
}
6671
}
72+
73+
/**
74+
* Sanitizes error response data to prevent leaking sensitive information
75+
* while preserving useful error details for debugging
76+
* @param data - error response data
77+
* @returns error as a string
78+
*/
79+
export function sanitizeErrorData(data: unknown): string {
80+
if (!data) {
81+
return 'Unknown error'
82+
}
83+
84+
// If it's already a string, check if it looks like sensitive data
85+
if (typeof data === 'string') {
86+
// Remove potential tokens, API keys, passwords, etc.
87+
return data
88+
.replace(
89+
/(?:token|key|password|secret|auth)['":\s]*["']?[A-Za-z0-9+/=._-]{10,}["']?/gi,
90+
'[REDACTED]'
91+
)
92+
.replace(/Bearer\s+[A-Za-z0-9+/=._-]+/gi, 'Bearer [REDACTED]')
93+
.trim()
94+
}
95+
96+
// If it's an object, extract safe error message
97+
if (typeof data === 'object' && data !== null) {
98+
const errorObj = data as Record<string, unknown>
99+
100+
// Common error message fields
101+
const messageFields = [
102+
'message',
103+
'error',
104+
'detail',
105+
'description',
106+
'reason',
107+
]
108+
109+
for (const field of messageFields) {
110+
if (typeof errorObj[field] === 'string') {
111+
return sanitizeErrorData(errorObj[field])
112+
}
113+
}
114+
115+
// If no message field found, return sanitized JSON string
116+
try {
117+
const sanitizedObj = sanitizeObject(errorObj)
118+
return JSON.stringify(sanitizedObj)
119+
} catch {
120+
return 'Error parsing server response'
121+
}
122+
}
123+
124+
// For other types, convert to string safely
125+
return String(data).replace(/[A-Za-z0-9+/=._-]{32,}/g, '[REDACTED]')
126+
}
127+
128+
/**
129+
* Recursively sanitizes an object, removing potentially sensitive values
130+
* @param obj - object to sanitize
131+
* @returns sanitized object
132+
*/
133+
function sanitizeObject(obj: Record<string, unknown>): Record<string, unknown> {
134+
const sanitized: Record<string, unknown> = {}
135+
136+
for (const [key, value] of Object.entries(obj)) {
137+
// Skip potentially sensitive keys
138+
const sensitiveKeys = [
139+
'token',
140+
'key',
141+
'password',
142+
'secret',
143+
'auth',
144+
'authorization',
145+
'credential',
146+
]
147+
if (
148+
sensitiveKeys.some(sensitiveKey =>
149+
key.toLowerCase().includes(sensitiveKey)
150+
)
151+
) {
152+
sanitized[key] = '[REDACTED]'
153+
continue
154+
}
155+
156+
if (typeof value === 'string') {
157+
sanitized[key] = sanitizeErrorData(value)
158+
} else if (typeof value === 'object' && value !== null) {
159+
if (Array.isArray(value)) {
160+
sanitized[key] = value.map(item =>
161+
typeof item === 'object' && item !== null
162+
? sanitizeObject(item as Record<string, unknown>)
163+
: sanitizeErrorData(item)
164+
)
165+
} else {
166+
sanitized[key] = sanitizeObject(value as Record<string, unknown>)
167+
}
168+
} else {
169+
sanitized[key] = value
170+
}
171+
}
172+
173+
return sanitized
174+
}

clients/javascript/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"@types/uuid": "10.0.0",
5454
"jest": "30.2.0",
5555
"jsonwebtoken": "9.0.2",
56+
"nock": "14.0.10",
5657
"ts-jest": "29.4.4",
5758
"tsup": "8.5.0",
5859
"tsx": "4.20.6",

0 commit comments

Comments
 (0)