Skip to content

Commit 0f690ba

Browse files
committed
🔧 fix: respect toResponse() method on Error classes
Fixes toResponse() being ignored when custom error classes extend Error, and status code being overridden when errors with toResponse() are thrown. The errorToResponse() functions now check for toResponse() before creating default Error responses, and the error handler extracts status from the Response returned by toResponse() before passing it through mapResponse()
1 parent 1084b5f commit 0f690ba

File tree

4 files changed

+124
-5
lines changed

4 files changed

+124
-5
lines changed

src/adapter/bun/handler.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -526,8 +526,14 @@ export const mapCompactResponse = (
526526
}
527527
}
528528

529-
export const errorToResponse = (error: Error, set?: Context['set']) =>
530-
new Response(
529+
export const errorToResponse = (error: Error, set?: Context['set']) => {
530+
// @ts-expect-error
531+
if (typeof error?.toResponse === 'function') {
532+
// @ts-expect-error
533+
return mapResponse(error.toResponse(), set ?? { headers: {}, status: 200, redirect: '' })
534+
}
535+
536+
return new Response(
531537
JSON.stringify({
532538
name: error?.name,
533539
message: error?.message,
@@ -539,6 +545,7 @@ export const errorToResponse = (error: Error, set?: Context['set']) =>
539545
headers: set?.headers as any
540546
}
541547
)
548+
}
542549

543550
export const createStaticHandler = (
544551
handle: unknown,

src/adapter/web-standard/handler.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -559,8 +559,14 @@ export const mapCompactResponse = (
559559
}
560560
}
561561

562-
export const errorToResponse = (error: Error, set?: Context['set']) =>
563-
new Response(
562+
export const errorToResponse = (error: Error, set?: Context['set']) => {
563+
// @ts-expect-error
564+
if (typeof error?.toResponse === 'function') {
565+
// @ts-expect-error
566+
return mapResponse(error.toResponse(), set ?? { headers: {}, status: 200, redirect: '' })
567+
}
568+
569+
return new Response(
564570
JSON.stringify({
565571
name: error?.name,
566572
message: error?.message,
@@ -572,6 +578,7 @@ export const errorToResponse = (error: Error, set?: Context['set']) =>
572578
headers: set?.headers as any
573579
}
574580
)
581+
}
575582

576583
export const createStaticHandler = (
577584
handle: unknown,

src/compose.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2598,6 +2598,17 @@ export const composeErrorHandler = (app: AnyElysia) => {
25982598
const saveResponse =
25992599
hasTrace || !!hooks.afterResponse?.length ? 'context.response = ' : ''
26002600

2601+
// Check for toResponse() first to handle errors with custom response methods
2602+
// This takes precedence over onError hooks, as the error defines its own response
2603+
// BUT exclude ValidationError and TransformDecodeError to allow proper validation error handling
2604+
fnLiteral +=
2605+
`if(typeof error?.toResponse==='function'&&error.constructor.name!=="ValidationError"&&error.constructor.name!=="TransformDecodeError"){` +
2606+
`const errorResponse=error.toResponse()\n` +
2607+
`if(errorResponse instanceof Response)set.status=errorResponse.status\n` +
2608+
afterResponse() +
2609+
`return context.response=context.responseValue=mapResponse(${saveResponse}errorResponse,set${adapter.mapResponseContext})\n` +
2610+
`}\n`
2611+
26012612
if (app.event.error)
26022613
for (let i = 0; i < app.event.error.length; i++) {
26032614
const handler = app.event.error[i]
@@ -2662,7 +2673,6 @@ export const composeErrorHandler = (app: AnyElysia) => {
26622673
fnLiteral +=
26632674
`if(error instanceof Error){` +
26642675
afterResponse() +
2665-
`\nif(typeof error.toResponse==='function')return context.response=context.responseValue=error.toResponse()\n` +
26662676
adapter.unknownError +
26672677
`\n}`
26682678

test/core/handle-error.test.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,4 +337,99 @@ describe('Handle Error', () => {
337337
expect(await res.text()).toBe('a')
338338
expect(res.status).toBe(500)
339339
})
340+
341+
it('handle Error with toResponse() when returned', async () => {
342+
class ErrorA extends Error {
343+
toResponse() {
344+
return Response.json({ error: 'hello' }, { status: 418 })
345+
}
346+
}
347+
348+
const app = new Elysia().get('/A', () => {
349+
return new ErrorA()
350+
})
351+
352+
const res = await app.handle(req('/A'))
353+
354+
expect(await res.json()).toEqual({ error: 'hello' })
355+
expect(res.status).toBe(418)
356+
})
357+
358+
it('handle Error with toResponse() when thrown', async () => {
359+
class ErrorA extends Error {
360+
toResponse() {
361+
return Response.json({ error: 'hello' }, { status: 418 })
362+
}
363+
}
364+
365+
const app = new Elysia().get('/A', () => {
366+
throw new ErrorA()
367+
})
368+
369+
const res = await app.handle(req('/A'))
370+
371+
expect(await res.json()).toEqual({ error: 'hello' })
372+
expect(res.status).toBe(418)
373+
})
374+
375+
it('handle non-Error with toResponse() when returned', async () => {
376+
class ErrorB {
377+
toResponse() {
378+
return Response.json({ error: 'hello' }, { status: 418 })
379+
}
380+
}
381+
382+
const app = new Elysia().get('/B', () => {
383+
return new ErrorB()
384+
})
385+
386+
const res = await app.handle(req('/B'))
387+
388+
expect(await res.json()).toEqual({ error: 'hello' })
389+
expect(res.status).toBe(418)
390+
})
391+
392+
it('handle non-Error with toResponse() when thrown', async () => {
393+
class ErrorB {
394+
toResponse() {
395+
return Response.json({ error: 'hello' }, { status: 418 })
396+
}
397+
}
398+
399+
const app = new Elysia().get('/B', () => {
400+
throw new ErrorB()
401+
})
402+
403+
const res = await app.handle(req('/B'))
404+
405+
expect(await res.json()).toEqual({ error: 'hello' })
406+
expect(res.status).toBe(418)
407+
})
408+
409+
it('handle Error with toResponse() that includes custom headers', async () => {
410+
class ErrorWithHeaders extends Error {
411+
toResponse() {
412+
return Response.json(
413+
{ error: 'custom error' },
414+
{
415+
status: 418,
416+
headers: {
417+
'X-Custom-Header': 'custom-value',
418+
'Content-Type': 'application/json; charset=utf-8'
419+
}
420+
}
421+
)
422+
}
423+
}
424+
425+
const app = new Elysia().get('/', () => {
426+
throw new ErrorWithHeaders()
427+
})
428+
429+
const res = await app.handle(req('/'))
430+
431+
expect(await res.json()).toEqual({ error: 'custom error' })
432+
expect(res.status).toBe(418)
433+
expect(res.headers.get('X-Custom-Header')).toBe('custom-value')
434+
})
340435
})

0 commit comments

Comments
 (0)