Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ jobs:
- name: Test
run: bun run test

- name: Test
run: bun run test:cf

- name: Publish Preview
if: github.event_name == 'pull_request'
run: bunx pkg-pr-new publish
3 changes: 3 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ jobs:
- name: Test
run: bun run test

- name: Test
run: bun run test:cf

- name: 'Publish'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
# 1.4.16 - 5 Nov 2025
Improvement:
- ValidationError: add `messageValue` as an alias of `errorValue`
- ValidationError.detail now accept optional 2nd parameter `allowUnsafeValidatorDetails`
- macro: add `introspect`
- prevent redundant route compilation
- merge multiple macro resolve response

Bug fix:
- [#1537](https://github.com/elysiajs/elysia/issues/1537) websocket: ping/pong not being called
- [#1536](https://github.com/elysiajs/elysia/pull/1536) export ExtractErrorFromHandle
- [#1535](https://github.com/elysiajs/elysia/pull/1535) skip response validation for generators and streams
- [#1531](https://github.com/elysiajs/elysia/pull/1531) typo in ElysiaTypeCustomErrorCallback: valdation to validation
- [#1528](https://github.com/elysiajs/elysia/issues/1528) error in parsing request body validation errors with Zod
- [#1527](https://github.com/elysiajs/elysia/issues/1527) bracket handling in exact mirror
- [#1524](https://github.com/elysiajs/elysia/pull/1524) head request handler not working

# 1.4.15 - 3 Nov 2025
Bug fix:
- 1.4.14 regression with Eden Treaty, and OpenAPI type gen
Expand Down
31 changes: 13 additions & 18 deletions example/a.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import Elysia, { t } from '../src'
import { Elysia, status, t } from '../src'

new Elysia()
.post(
'/mirror',
async ({ status, body }) => status(201, { success: false }),
{
body: t.Object({
code: t.String()
}),
response: {
200: t.Object({
success: t.Literal(true)
}),
201: t.Object({
success: t.Literal(false)
})
status(401)

const app = new Elysia()
.macro({
multiple: {
resolve({ status }) {
if (Math.random() > 0.5) return status(401)
return status(403)
}
}
)
.listen(3333)
})
.get('/multiple', () => 'Ok', { multiple: true })

app['~Routes']['multiple']['get']['response']
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@
],
"license": "MIT",
"scripts": {
"test": "bun run test:functionality && bun run test:types && bun run test:node && bun run test:cf",
"test": "bun run test:functionality && bun run test:types && bun run test:node",
"test:functionality": "bun test && bun run test:imports",
"test:imports": "bun run test/type-system/import.ts",
"test:types": "tsc --project tsconfig.test.json",
Expand All @@ -191,7 +191,7 @@
},
"dependencies": {
"cookie": "^1.0.2",
"exact-mirror": "0.2.2",
"exact-mirror": "0.2.3",
"fast-decode-uri-component": "^1.0.1",
"memoirist": "^0.4.0"
},
Expand Down
5 changes: 2 additions & 3 deletions src/adapter/bun/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { createBunRouteHandler } from './compose'
import { createNativeStaticHandler } from './handler-native'

import { serializeCookie } from '../../cookies'
import { isProduction, ValidationError } from '../../error'
import { isProduction, status, ValidationError } from '../../error'
import { getSchemaValidator } from '../../schema'
import {
hasHeaderShorthand,
Expand Down Expand Up @@ -627,8 +627,7 @@ export const BunAdapter: ElysiaAdapter = {
)
return

set.status = 400
return 'Expected a websocket connection'
return status(400, 'Expected a websocket connection')
},
{
...rest,
Expand Down
6 changes: 2 additions & 4 deletions src/adapter/web-standard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,8 @@ export const WebStandardAdapter: ElysiaAdapter = {
fnLiteral +=
`const u=r.url,` +
`s=u.indexOf('/',${standardHostname ? 11 : 7}),` +
`qi=u.indexOf('?',s+1)\n` +
`let p\n` +
`if(qi===-1)p=u.substring(s)\n` +
`else p=u.substring(s, qi)\n`
`qi=u.indexOf('?',s+1),` +
`p=u.substring(s,qi===-1?undefined:qi)\n`

if (hasTrace) fnLiteral += `const id=randomId()\n`

Expand Down
46 changes: 39 additions & 7 deletions src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const emptyHttpStatus = {
308: undefined
} as const

export type SelectiveStatus<Res> = <
export type SelectiveStatus<in out Res> = <
const Code extends
| keyof Res
| InvertedStatusMap[Extract<keyof InvertedStatusMap, keyof Res>]
Expand Down Expand Up @@ -91,7 +91,7 @@ export const status = <
>(
code: Code,
response?: T
) => new ElysiaCustomStatusResponse<Code, T>(code, response as any)
) => new ElysiaCustomStatusResponse<Code, T>(code, response as T)

export class InternalServerError extends Error {
code = 'INTERNAL_SERVER_ERROR'
Expand Down Expand Up @@ -153,9 +153,15 @@ export const mapValueError = (error: ValueError | undefined): MapValueError => {
summary: undefined
}

const { message, path, value, type } = error
let { message, path, value, type } = error

if (Array.isArray(path)) path = path[0]

const property =
typeof path === 'string'
? path.slice(1).replaceAll('/', '.')
: 'unknown'

const property = path.slice(1).replaceAll('/', '.')
const isRoot = path === ''
Comment on lines +156 to 165
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restore correct property names when handling array paths

When path comes in as an array (e.g., Zod issues return ['channel']), we collapse it to the first element and then call slice(1). That trims the first character, so "channel" is rendered as "hannel" and "otpTo" as "tpTo". The new Cloudflare test surfaces this regression in the response summary. Please preserve the full segment and only trim a leading slash when one is present.

I suggest transforming array paths into a dotted string and normalizing slashes conditionally:

- if (Array.isArray(path)) path = path[0]
-
- const property =
-     typeof path === 'string'
-         ? path.slice(1).replaceAll('/', '.')
-         : 'unknown'
+ if (Array.isArray(path))
+     path = path
+         .map((segment) =>
+             typeof segment === 'number' ? `[${segment}]` : segment
+         )
+         .join('.')
+
+ const property =
+     typeof path === 'string'
+         ? (path.startsWith('/') ? path.slice(1) : path).replaceAll('/', '.')
+         : 'unknown'

This keeps existing TypeBox behavior, fixes Zod summaries, and still handles slash-prefixed paths.

🤖 Prompt for AI Agents
In src/error.ts around lines 156-165, the code incorrectly trims the first
character of property names when path is an array (e.g., "channel" -> "hannel");
fix by first normalizing path into a string (if Array.isArray(path) join
segments with '.'), then only strip a leading slash when present (if the
normalized string startsWith('/') remove that single leading char), and finally
replace internal '/' with '.' to produce the property name; compute isRoot
against the normalized path (empty string) after this normalization.


switch (type) {
Expand Down Expand Up @@ -271,8 +277,29 @@ export class ValidationError extends Error {
code = 'VALIDATION'
status = 422

/**
* An actual value of `message`
*
* Since `message` is string
* use this instead of message
*/
valueError?: ValueError

/**
* Alias of `valueError`
*/
get messageValue() {
return this.valueError
}

/**
* Expected value of the schema
*/
expected?: unknown

/**
* Custom error if provided
*/
customError?: string

constructor(
Expand All @@ -282,6 +309,9 @@ export class ValidationError extends Error {
| TypeCheck<any>
| ElysiaTypeCheck<any>
| StandardSchemaV1Like,
/**
* Input value
*/
public value: unknown,
private allowUnsafeValidationDetails = false,
errors?: ValueErrorIterator
Expand Down Expand Up @@ -535,15 +565,17 @@ export class ValidationError extends Error {
* })
* ```
*/
detail(message: unknown) {
detail(
message: unknown,
allowUnsafeValidatorDetails = this.allowUnsafeValidationDetails
) {
if (!this.customError) return this.message

const validator = this.validator
const value = this.value
const expected = this.expected
const errors = this.all

return isProduction && !this.allowUnsafeValidationDetails
return isProduction && !allowUnsafeValidatorDetails
? {
type: 'validation',
on: this.type,
Expand Down
42 changes: 30 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -909,11 +909,8 @@ export default class Elysia<

addResponsePath(path)

let _compiled: ComposedHandler
const compile = () => {
if (_compiled) return _compiled

return (_compiled = composeHandler({
const compiled = composeHandler({
app: this,
path,
method,
Expand All @@ -926,7 +923,12 @@ export default class Elysia<
: handle,
allowMeta,
inference: this.inference
}))
})

if (this.router.history[index])
this.router.history[index].composed = compiled

return compiled
}

let oldIndex: number | undefined
Expand All @@ -941,22 +943,22 @@ export default class Elysia<
else this.routeTree[`${method}_${path}`] = this.router.history.length

const index = oldIndex ?? this.router.history.length
const route = this.router.history

const mainHandler = shouldPrecompile
? compile()
: (ctx: Context) =>
(
(this.router.history[index].composed =
compile!()) as ComposedHandler
)(ctx)
((route[index].composed = compile!()) as ComposedHandler)(
ctx
)

if (oldIndex !== undefined)
this.router.history[oldIndex] = Object.assign(
{
method,
path,
composed: mainHandler,
compile: compile!,
compile,
handler: handle,
hooks
},
Expand All @@ -976,7 +978,7 @@ export default class Elysia<
method,
path,
composed: mainHandler,
compile: compile!,
compile,
handler: handle,
hooks
},
Expand All @@ -987,7 +989,9 @@ export default class Elysia<
)

const handler = {
handler: shouldPrecompile ? mainHandler : undefined,
handler: shouldPrecompile
? (route[index].composed as ComposedHandler)
: undefined,
compile() {
return (this.handler = compile!())
}
Expand Down Expand Up @@ -5235,6 +5239,10 @@ export default class Elysia<
>,
const Property extends MaybeValueOrVoidFunction<
MacroProperty<
Metadata['macro'] &
InputSchema<keyof Definitions['typebox'] & string> & {
[name in Name]?: boolean
},
Schema & MacroContext,
Singleton & {
derive: Partial<Ephemeral['derive'] & Volatile['derive']>
Expand Down Expand Up @@ -5277,6 +5285,8 @@ export default class Elysia<
const Input extends Metadata['macro'] &
InputSchema<keyof Definitions['typebox'] & string>,
const NewMacro extends Macro<
Metadata['macro'] &
InputSchema<keyof Definitions['typebox'] & string>,
Input,
IntersectIfObjectSchema<
MergeSchema<
Expand Down Expand Up @@ -5321,6 +5331,7 @@ export default class Elysia<
const NewMacro extends MaybeFunction<
Macro<
Input,
// @ts-ignore trust me bro
IntersectIfObjectSchema<
MergeSchema<
UnwrapRoute<Input, Definitions['typebox'], BasePath>,
Expand Down Expand Up @@ -5417,6 +5428,13 @@ export default class Elysia<
continue
}

if (k === 'introspect') {
value?.(localHook)

delete localHook[key]
continue
}

if (k === 'detail') {
if (!localHook.detail) localHook.detail = {}
localHook.detail = mergeDeep(localHook.detail, value, {
Expand Down
14 changes: 11 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -966,7 +966,7 @@ type ExtractOnlyResponseFromMacro<A> =
? IsNever<A> extends true
? {}
: {
return: A extends ElysiaCustomStatusResponse<
return: UnionToIntersect< A extends ElysiaCustomStatusResponse<
any,
infer Value,
infer Status
Expand All @@ -977,7 +977,7 @@ type ExtractOnlyResponseFromMacro<A> =
InvertedStatusMap[Status]
: Value
}
: {}
: {}>
}
: {}

Expand Down Expand Up @@ -1944,6 +1944,7 @@ export type BaseMacro = Record<
export type MaybeValueOrVoidFunction<T> = T | ((...a: any) => void | T)

export interface MacroProperty<
in out Macro extends BaseMacro = {},
in out TypedRoute extends RouteSchema = {},
in out Singleton extends SingletonBase = {
decorator: {}
Expand All @@ -1966,9 +1967,16 @@ export interface MacroProperty<
afterResponse?: MaybeArray<AfterResponseHandler<TypedRoute, Singleton>>
resolve?: MaybeArray<ResolveHandler<TypedRoute, Singleton>>
detail?: DocumentDecoration
/**
* Introspect hook option for documentation generation or analysis
*
* @param option
*/
introspect?(option: Prettify<Macro>): unknown
}

export interface Macro<
in out Macro extends BaseMacro = {},
in out Input extends BaseMacro = {},
in out TypedRoute extends RouteSchema = {},
in out Singleton extends SingletonBase = {
Expand All @@ -1980,7 +1988,7 @@ export interface Macro<
in out Errors extends Record<string, Error> = {}
> {
[K: keyof any]: MaybeValueOrVoidFunction<
Input & MacroProperty<TypedRoute, Singleton, Errors>
Input & MacroProperty<Macro, TypedRoute, Singleton, Errors>
>
}

Expand Down
Loading
Loading