Skip to content

Commit

Permalink
🔧 fix: handle error thrown as error
Browse files Browse the repository at this point in the history
  • Loading branch information
SaltyAom committed Dec 27, 2024
1 parent e1100bf commit b94dab2
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 47 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
Bug fix:
- macro doesn't work with guard
- [#981](https://github.com/elysiajs/elysia/issues/981) unable to deference schema, create default, and coerce value
- [#966](https://github.com/elysiajs/elysia/issues/966) `error`'s value return as-if when thrown
- [#964](https://github.com/elysiajs/elysia/issues/964) InvalidCookieSignature errors are not caught by onError
- [#952](https://github.com/elysiajs/elysia/issues/952) onAfterResponse does not provide mapped response value unless aot is disabled
- `mapResponse.response` is `{}` if no response schema is provided
- Response doesn't reconcile when handler return `Response` is used with `mapResponse`

# 1.2.6 - 25 Dec 2024
Bug fix:
Expand Down
22 changes: 13 additions & 9 deletions example/a.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { Elysia, t } from '../src'
import { Elysia, t, error } from '../src'
import { req } from '../test/utils'

const app = new Elysia({ aot: false })
.onAfterResponse(({ response }) => {
console.log('Response:', response)
const res = await new Elysia({
aot: false
})
.get('/', () => 'Hi')
.onError(({ code }) => {
if (code === 'NOT_FOUND')
return new Response("I'm a teapot", {
status: 418
})
})
.get('/', async () => {
return { ok: true }
})
.listen(3000)
.handle(req('/not-found'))

app.handle(req('/'))
// console.log(await res.text())
// console.log(res.status)
6 changes: 6 additions & 0 deletions src/adapter/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ export interface ElysiaAdapter {
inject?: Record<string, unknown>
mapResponseContext: string
validationError: string
/**
* Handle thrown error which is instance of Error
*
* Despite its name of `unknownError`, it also handle named error like `NOT_FOUND`, `VALIDATION_ERROR`
* It's named `unknownError` because it also catch unknown error
*/
unknownError: string
}
ws?(app: AnyElysia, path: string, handler: AnyWSLocalHook): unknown
Expand Down
34 changes: 28 additions & 6 deletions src/adapter/web-standard/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,17 @@ export const mergeResponseWithSetHeaders = (
response: Response,
set: Context['set']
) => {
if (
(response as Response).status !== set.status &&
set.status !== 200 &&
((response.status as number) <= 300 ||
(response.status as number) > 400)
)
response = new Response(response.body, {
headers: response.headers,
status: set.status as number
})

let isCookieSet = false

if (set.headers instanceof Headers)
Expand All @@ -246,8 +257,7 @@ export const mergeResponseWithSetHeaders = (
for (const key in set.headers)
(response as Response).headers.append(key, set.headers[key] as any)

if ((response as Response).status !== set.status)
set.status = (response as Response).status
return response
}

export const mapResponse = (
Expand Down Expand Up @@ -311,7 +321,10 @@ export const mapResponse = (
return Response.json(response, set as any)

case 'Response':
mergeResponseWithSetHeaders(response as Response, set)
response = mergeResponseWithSetHeaders(
response as Response,
set
)

if (
(response as Response).headers.get('transfer-encoding') ===
Expand Down Expand Up @@ -354,7 +367,10 @@ export const mapResponse = (

default:
if (response instanceof Response) {
mergeResponseWithSetHeaders(response as Response, set)
response = mergeResponseWithSetHeaders(
response as Response,
set
)

if (
(response as Response).headers.get(
Expand Down Expand Up @@ -496,7 +512,10 @@ export const mapEarlyResponse = (
return Response.json(response, set as any)

case 'Response':
mergeResponseWithSetHeaders(response as Response, set)
response = mergeResponseWithSetHeaders(
response as Response,
set
)

if (
(response as Response).headers.get('transfer-encoding') ===
Expand Down Expand Up @@ -540,7 +559,10 @@ export const mapEarlyResponse = (

default:
if (response instanceof Response) {
mergeResponseWithSetHeaders(response as Response, set)
response = mergeResponseWithSetHeaders(
response as Response,
set
)

if (
(response as Response).headers.get(
Expand Down
34 changes: 19 additions & 15 deletions src/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2279,13 +2279,13 @@ export const composeErrorHandler = (app: AnyElysia) => {
})

fnLiteral +=
`const set = context.set\n` +
`const set=context.set\n` +
`let _r\n` +
`if(!context.code)context.code=error.code??error[ERROR_CODE]\n` +
`if(!(context.error instanceof Error))context.error = error\n` +
`if(!(context.error instanceof Error))context.error=error\n` +
`if(error instanceof ElysiaCustomStatusResponse){` +
`error.status = error.code\n` +
`error.message = error.response` +
`set.status=error.status=error.code\n` +
`error.message=error.response` +
`}`

if (adapter.declare) fnLiteral += adapter.declare
Expand Down Expand Up @@ -2314,7 +2314,7 @@ export const composeErrorHandler = (app: AnyElysia) => {
`error.status=error.code\n` +
`error.message = error.response` +
`}` +
`if(set.status === 200) set.status = error.status\n`
`if(set.status===200||!set.status)set.status=error.status\n`

const mapResponseReporter = report('mapResponse', {
total: hooks.mapResponse.length,
Expand Down Expand Up @@ -2348,19 +2348,23 @@ export const composeErrorHandler = (app: AnyElysia) => {
fnLiteral +=
`if(error.constructor.name==="ValidationError"||error.constructor.name==="TransformDecodeError"){` +
`if(error.error)error=error.error\n` +
`set.status = error.status??422\n` +
`set.status=error.status??422\n` +
adapter.validationError +
`}else{` +
`if(error.code&&typeof error.status==="number"){` +
adapter.unknownError +
'}'
`}`

fnLiteral += `if(error instanceof Error){` + adapter.unknownError + `}`

const mapResponseReporter = report('mapResponse', {
total: hooks.mapResponse.length,
name: 'context'
})

fnLiteral +=
'\nif(!context.response)context.response=error.message??error\n'

if (hooks.mapResponse.length) {
fnLiteral += 'let mr\n'

for (let i = 0; i < hooks.mapResponse.length; i++) {
const mapResponse = hooks.mapResponse[i]

Expand All @@ -2369,18 +2373,18 @@ export const composeErrorHandler = (app: AnyElysia) => {
)

fnLiteral +=
`context.response=error\n` +
`error=${
isAsyncName(mapResponse) ? 'await ' : ''
}onMapResponse[${i}](context)\n`
`if(mr===undefined){` +
`mr=${isAsyncName(mapResponse) ? 'await ' : ''}onMapResponse[${i}](context)\n` +
`if(mr!==undefined)error=context.response=mr` +
'}'

endUnit()
}
}

mapResponseReporter.resolve()

fnLiteral += `\nreturn mapResponse(${saveResponse}error,set${adapter.mapResponseContext})}}`
fnLiteral += `\nreturn mapResponse(${saveResponse}error,set${adapter.mapResponseContext})}`

return Function(
'inject',
Expand Down
9 changes: 6 additions & 3 deletions src/dynamic-handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ const injectDefaultValues = (
typeChecker: TypeCheck<any>,
obj: Record<string, any>
) => {
// @ts-expect-error private
for (const [key, keySchema] of Object.entries(
// @ts-expect-error private
typeChecker.schema.properties
)) {
// @ts-expect-error private
Expand Down Expand Up @@ -459,8 +459,11 @@ export const createDynamicHandler = (app: AnyElysia) => {
error instanceof TransformDecodeError && error.error
? error.error
: error
if ((reportedError as ElysiaErrors).status)
set.status = (reportedError as ElysiaErrors).status

// ? Since error is reconciled in mergeResponseWithHeaders, this is not needed (if I'm not drunk)
// if ((reportedError as ElysiaErrors).status)
// set.status = (reportedError as ElysiaErrors).status

// @ts-expect-error private
return app.handleError(context, reportedError)
} finally {
Expand Down
34 changes: 23 additions & 11 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -750,17 +750,29 @@ export type MapResponse<
resolve: {}
},
Path extends string | undefined = undefined
> = Handler<
Omit<Route, 'response'> & {
response: {} extends Route['response'] ? unknown : Route['response']
},
Singleton & {
derive: {
response: Route['response']
}
},
Path
>
> = (
context: Context<
Omit<Route, 'response'> & {},
Singleton & {
derive: {
response: {} extends Route['response']
? unknown
: Route['response']
}
},
Path
>
) => MaybePromise<Response | void>

// Handler<
// Omit<Route, 'response'> & {},
// Singleton & {
// derive: {
// response: {} extends Route['response'] ? unknown : Route['response']
// }
// },
// Path
// >

export type VoidHandler<
in out Route extends RouteSchema = {},
Expand Down
64 changes: 61 additions & 3 deletions test/core/handle-error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ describe('Handle Error', () => {
})

it('inject headers to error', async () => {
const app = new Elysia({
forceErrorEncapsulation: true
})
const app = new Elysia()
.onRequest(({ set }) => {
set.headers['Access-Control-Allow-Origin'] = '*'
})
Expand Down Expand Up @@ -248,4 +246,64 @@ describe('Handle Error', () => {
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({ error }) {
throw error("I'm a teapot", { message: 'meow!' })
}
})
// @ts-expect-error private property
.handleError(
{
request: new Request('http://localhost/'),
set: {
headers: {}
}
},
error(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({ error }) {
throw error("I'm a teapot", { message: 'meow!' })
}
})
// @ts-expect-error private property
.handleError(
{
request: new Request('http://localhost/'),
set: {
headers: {}
}
},
error(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)
})
})

0 comments on commit b94dab2

Please sign in to comment.