Files
elysia/test/core/handle-error.test.ts
2025-11-15 16:42:16 +01:00

660 lines
16 KiB
TypeScript

import { Elysia, InternalServerError, NotFoundError, status, t } from '../../src'
import { describe, expect, it } from 'bun:test'
import { req } from '../utils'
const request = new Request('http://localhost:8080')
describe('Handle Error', () => {
it('handle NOT_FOUND', async () => {
const res = await new Elysia()
.get('/', () => 'Hi')
// @ts-expect-error private
.handleError(
{
request,
set: {
headers: {}
}
},
new NotFoundError()
)
expect(await res.text()).toBe('NOT_FOUND')
expect(res.status).toBe(404)
})
it('handle INTERNAL_SERVER_ERROR', async () => {
const res = await new Elysia()
.get('/', () => 'Hi')
// @ts-expect-error private
.handleError(
{
request,
set: {
headers: {}
}
},
new InternalServerError()
)
expect(await res.text()).toBe('INTERNAL_SERVER_ERROR')
expect(res.status).toBe(500)
})
it('handle VALIDATION', async () => {
const res = await new Elysia()
.get('/', () => 'Hi', {
query: t.Object({
name: t.String()
})
})
.handle(req('/'))
expect(res.status).toBe(422)
})
it('use custom error', async () => {
const res = await new Elysia()
.get('/', () => 'Hi')
.onError(({ code }) => {
if (code === 'NOT_FOUND')
return new Response("I'm a teapot", {
status: 418
})
})
.handle(req('/not-found'))
expect(await res.text()).toBe("I'm a teapot")
expect(res.status).toBe(418)
})
it('inject headers to error', async () => {
const app = new Elysia()
.onRequest(({ set }) => {
set.headers['Access-Control-Allow-Origin'] = '*'
})
.get('/', () => {
throw new NotFoundError()
})
const res = await app.handle(req('/'))
expect(res.headers.get('Access-Control-Allow-Origin')).toBe('*')
expect(res.status).toBe(404)
})
it('transform any to error', async () => {
const app = new Elysia()
.onError(async ({ set }) => {
set.status = 418
return 'aw man'
})
.get('/', () => {
throw new NotFoundError()
})
const res = await app.handle(req('/'))
expect(await res.text()).toBe('aw man')
expect(res.status).toBe(418)
})
it('handle error in group', async () => {
const authenticate = new Elysia().group('/group', (group) =>
group
.get('/inner', () => {
throw new Error('A')
})
.onError(() => {
return 'handled'
})
)
const app = new Elysia().use(authenticate)
const response = await app.handle(req('/group/inner'))
expect(await response.text()).toEqual('handled')
expect(response.status).toEqual(500)
})
it('handle error status in group', async () => {
const authenticate = new Elysia().group('/group', (group) =>
group
.get('/inner', ({ set }) => {
set.status = 418
throw new Error('A')
})
.onError(() => {
return 'handled'
})
)
const app = new Elysia().use(authenticate)
const response = await app.handle(req('/group/inner'))
expect(await response.text()).toEqual('handled')
expect(response.status).toEqual(418)
})
it('handle thrown error function', async () => {
const app = new Elysia().get('/', ({ status }) => {
throw status(404, 'Not Found :(')
})
const response = await app.handle(req('/'))
expect(await response.text()).toEqual('Not Found :(')
expect(response.status).toEqual(404)
})
it('handle thrown Response', async () => {
const app = new Elysia().get('/', ({ status }) => {
throw status(404, 'Not Found :(')
})
const response = await app.handle(req('/'))
expect(await response.text()).toEqual('Not Found :(')
expect(response.status).toEqual(404)
})
it('handle error code in request', async () => {
class APIError extends Error {
public readonly message: string
public readonly status: number
constructor(
status: number,
message: string,
options?: ErrorOptions
) {
super(message, options)
this.status = status
this.message = message
this.name = 'APIError'
Object.setPrototypeOf(this, APIError.prototype)
Error.captureStackTrace(this)
}
}
const errors = new Elysia()
.error({ APIError })
.onError({ as: 'global' }, ({ code }) => {
return code
})
const requestHandler = new Elysia()
.onTransform(() => {
throw new APIError(403, 'Not authorized')
})
.get('/', () => 'a')
const app = new Elysia().use(errors).use(requestHandler)
expect(await app.handle(req('/')).then((req) => req.text())).toBe(
'APIError'
)
})
it('parse headers', async () => {
const headers = await new Elysia()
.get('/', ({ headers }) => headers)
.handle(
new Request('http://localhost:3000', {
headers: {
'Content-Type': 'application/json',
'X-Test': 'Nagi'
}
})
)
.then((x) => x.json())
expect(headers).toEqual({
'content-type': 'application/json',
'x-test': 'Nagi'
})
})
it('handle error in Transform', async () => {
const route = new Elysia().get('/', ({ query: { aid } }) => aid, {
query: t.Object({
aid: t
.Transform(t.String())
.Decode((value) => {
throw new NotFoundError('foo')
})
.Encode((value) => `1`)
})
})
let response = await new Elysia({ aot: false })
.use(route)
.handle(req('/?aid=a'))
expect(response.status).toEqual(404)
expect(await response.text()).toEqual('foo')
response = await new Elysia({ aot: true })
.use(route)
.handle(req('/?aid=a'))
expect(response.status).toEqual(404)
expect(await response.text()).toEqual('foo')
})
it('map status error to response', async () => {
const value = { message: 'meow!' }
const response: Response = await new Elysia()
.get('/', () => 'Hello', {
beforeHandle({ status }) {
throw status("I'm a teapot", { message: 'meow!' })
}
})
// @ts-expect-error private property
.handleError(
{
request: new Request('http://localhost/'),
set: {
headers: {}
}
},
status(422, value) as any
)
expect(await response.json()).toEqual(value)
expect(response.headers.get('content-type')).toStartWith(
'application/json'
)
expect(response.status).toEqual(422)
})
it('map status error with custom mapResponse', async () => {
const value = { message: 'meow!' }
const response: Response = await new Elysia()
.mapResponse(({ response }) => {
if (typeof response === 'object')
return new Response('Don Quixote', {
headers: {
'content-type': 'text/plain'
}
})
})
.get('/', () => 'Hello', {
beforeHandle({ status }) {
throw status("I'm a teapot", { message: 'meow!' })
}
})
// @ts-expect-error private property
.handleError(
{
request: new Request('http://localhost/'),
set: {
headers: {}
}
},
status(422, value) as any
)
expect(await response.text()).toBe('Don Quixote')
expect(response.headers.get('content-type')).toStartWith('text/plain')
expect(response.status).toEqual(422)
})
it('handle generic error', async () => {
const res = await new Elysia()
.get('/', () => 'Hi')
// @ts-expect-error private
.handleError(
{
request,
set: {
headers: {}
}
},
// https://youtube.com/shorts/PbIWVPKHfrQ
new Error('a')
)
expect(await res.text()).toBe('a')
expect(res.status).toBe(500)
})
it('handle generic error when thrown in handler', async () => {
const app = new Elysia().get('/', () => {
throw new Error('a')
})
const res = await app.handle(req('/'))
expect(await res.text()).toBe('a')
expect(res.status).toBe(500)
})
it('handle Error with toResponse() when returned', async () => {
class ErrorA extends Error {
toResponse() {
return Response.json({ error: 'hello' }, { status: 418 })
}
}
const app = new Elysia().get('/A', () => {
return new ErrorA()
})
const res = await app.handle(req('/A'))
expect(await res.json()).toEqual({ error: 'hello' })
expect(res.status).toBe(418)
})
it('handle Error with toResponse() when thrown', async () => {
class ErrorA extends Error {
toResponse() {
return Response.json({ error: 'hello' }, { status: 418 })
}
}
const app = new Elysia().get('/A', () => {
throw new ErrorA()
})
const res = await app.handle(req('/A'))
expect(await res.json()).toEqual({ error: 'hello' })
expect(res.status).toBe(418)
})
it('handle non-Error with toResponse() when returned', async () => {
class ErrorB {
toResponse() {
return Response.json({ error: 'hello' }, { status: 418 })
}
}
const app = new Elysia().get('/B', () => {
return new ErrorB()
})
const res = await app.handle(req('/B'))
expect(await res.json()).toEqual({ error: 'hello' })
expect(res.status).toBe(418)
})
it('handle non-Error with toResponse() when thrown', async () => {
class ErrorB {
toResponse() {
return Response.json({ error: 'hello' }, { status: 418 })
}
}
const app = new Elysia().get('/B', () => {
throw new ErrorB()
})
const res = await app.handle(req('/B'))
expect(await res.json()).toEqual({ error: 'hello' })
expect(res.status).toBe(418)
})
it('handle Error with toResponse() that includes custom headers', async () => {
class ErrorWithHeaders extends Error {
toResponse() {
return Response.json(
{ error: 'custom error' },
{
status: 418,
headers: {
'X-Custom-Header': 'custom-value',
'Content-Type': 'application/json; charset=utf-8'
}
}
)
}
}
const app = new Elysia().get('/', () => {
throw new ErrorWithHeaders()
})
const res = await app.handle(req('/'))
expect(await res.json()).toEqual({ error: 'custom error' })
expect(res.status).toBe(418)
expect(res.headers.get('X-Custom-Header')).toBe('custom-value')
})
it('handle async toResponse() when thrown', async () => {
class AsyncError extends Error {
async toResponse() {
// Simulate async operation
await new Promise(resolve => setTimeout(resolve, 10))
return Response.json({ error: 'async error' }, { status: 418 })
}
}
const app = new Elysia().get('/', () => {
throw new AsyncError()
})
const res = await app.handle(req('/'))
expect(await res.json()).toEqual({ error: 'async error' })
expect(res.status).toBe(418)
})
it('handle async toResponse() when returned', async () => {
class AsyncError extends Error {
async toResponse() {
// Simulate async operation
await new Promise(resolve => setTimeout(resolve, 10))
return Response.json({ error: 'async error' }, { status: 418 })
}
}
const app = new Elysia().get('/', () => {
return new AsyncError()
})
const res = await app.handle(req('/'))
expect(await res.json()).toEqual({ error: 'async error' })
expect(res.status).toBe(418)
})
it('handle async toResponse() with custom headers', async () => {
class AsyncErrorWithHeaders extends Error {
async toResponse() {
await new Promise(resolve => setTimeout(resolve, 10))
return Response.json(
{ error: 'async with headers' },
{
status: 419,
headers: {
'X-Async-Header': 'async-value'
}
}
)
}
}
const app = new Elysia().get('/', () => {
throw new AsyncErrorWithHeaders()
})
const res = await app.handle(req('/'))
expect(await res.json()).toEqual({ error: 'async with headers' })
expect(res.status).toBe(419)
expect(res.headers.get('X-Async-Header')).toBe('async-value')
})
it('handle non-Error with async toResponse()', async () => {
class AsyncNonError {
async toResponse() {
await new Promise(resolve => setTimeout(resolve, 10))
return Response.json({ error: 'non-error async' }, { status: 418 })
}
}
const app = new Elysia().get('/', () => {
throw new AsyncNonError()
})
const res = await app.handle(req('/'))
expect(await res.json()).toEqual({ error: 'non-error async' })
expect(res.status).toBe(418)
})
it('handle toResponse() that throws an error', async () => {
class BrokenError extends Error {
toResponse() {
throw new Error('toResponse failed')
}
}
const app = new Elysia().get('/', () => {
throw new BrokenError('original error')
})
const res = await app.handle(req('/'))
expect(res.status).toBe(500)
expect(await res.text()).toBe('original error')
})
it('handle async toResponse() that throws an error', async () => {
class BrokenAsyncError extends Error {
async toResponse() {
throw new Error('async toResponse failed')
}
}
const app = new Elysia().get('/', () => {
throw new BrokenAsyncError('original error')
})
const res = await app.handle(req('/'))
expect(res.status).toBe(500)
expect(await res.text()).toBe('original error')
})
it('send set-cookie header when error is thrown', async () => {
const app = new Elysia().get('/', ({ cookie }) => {
cookie.session.value = 'test-session-id'
throw new Error('test error')
})
const res = await app.handle(req('/'))
expect(res.status).toBe(500)
expect(res.headers.get('set-cookie')).toContain('session=test-session-id')
})
it('send set-cookie header when response validation error occurs', async () => {
const app = new Elysia().get('/', ({ cookie }) => {
cookie.session.value = 'test-session-id'
return 'invalid response'
}, {
response: t.Number()
})
const res = await app.handle(req('/'))
expect(res.status).toBe(422)
expect(res.headers.get('set-cookie')).toContain('session=test-session-id')
})
it('send set-cookie header when error is thrown with onError hook', async () => {
const app = new Elysia()
.onError(({ error }) => {
return error.message
})
.get('/', ({ cookie }) => {
cookie.session.value = 'test-session-id'
throw new Error('custom error')
})
const res = await app.handle(req('/'))
expect(res.status).toBe(500)
expect(await res.text()).toBe('custom error')
expect(res.headers.get('set-cookie')).toContain('session=test-session-id')
})
it('send set-cookie header when NotFoundError is thrown', async () => {
const app = new Elysia().get('/', ({ cookie }) => {
cookie.session.value = 'test-session-id'
throw new NotFoundError()
})
const res = await app.handle(req('/'))
expect(res.status).toBe(404)
expect(res.headers.get('set-cookie')).toContain('session=test-session-id')
})
it('send set-cookie header when InternalServerError is thrown', async () => {
const app = new Elysia().get('/', ({ cookie }) => {
cookie.session.value = 'test-session-id'
throw new InternalServerError()
})
const res = await app.handle(req('/'))
expect(res.status).toBe(500)
expect(res.headers.get('set-cookie')).toContain('session=test-session-id')
})
it('send set-cookie header in AOT mode when error is thrown', async () => {
const app = new Elysia({ aot: true }).get('/', ({ cookie }) => {
cookie.session.value = 'test-session-id'
throw new Error('test error')
})
const res = await app.handle(req('/'))
expect(res.status).toBe(500)
expect(res.headers.get('set-cookie')).toContain('session=test-session-id')
})
it('preserve multiple cookies when error is thrown', async () => {
const app = new Elysia().get('/', ({ cookie }) => {
cookie.session.value = 'session-123'
cookie.user.value = 'user-456'
throw new Error('test error')
})
const res = await app.handle(req('/'))
expect(res.status).toBe(500)
const setCookie = res.headers.get('set-cookie')
expect(setCookie).toContain('session=session-123')
expect(setCookie).toContain('user=user-456')
})
it('send set-cookie header when error has custom headers', async () => {
const app = new Elysia().get('/', ({ cookie, set }) => {
cookie.session.value = 'test-session-id'
set.headers['x-custom'] = 'value'
throw new Error('test error')
})
const res = await app.handle(req('/'))
expect(res.headers.get('set-cookie')).toContain('session=test-session-id')
expect(res.headers.get('x-custom')).toBe('value')
})
})