mirror of
https://github.com/zoriya/elysia.git
synced 2026-08-15 18:44:25 +00:00
660 lines
16 KiB
TypeScript
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')
|
|
})
|
|
})
|