Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
# 1.4.16 - 5 Nov 2025
Improvement:
- ValidationError: add `messageValue` as an alias of `errorValue`
- ValidationError.detail now accept optional 2nd parameter `allowUnsafeValidatorDetails`

Bug fix:
- [#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

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

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)
})
}
}
)
.listen(3333)
.ws("/", {
ping() {
console.log("onping")
},

pong() {
console.log("onpong")
},

async message(ws) {
console.log("onmessage")
console.log(ws.body)
},
})
.listen(3005)

const ws = new WebSocket("ws://localhost:3005")

ws.addEventListener("open", () => {
ws.ping()
ws.send("df")
ws.ping()
})
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
42 changes: 37 additions & 5 deletions src/error.ts
Original file line number Diff line number Diff line change
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
6 changes: 6 additions & 0 deletions src/ws/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ export const websocket: WebSocketHandler<any> = {
},
close(ws, code, reason) {
ws.data.close?.(ws, code, reason)
},
ping(ws) {
ws.data.ping?.(ws)
},
pong(ws) {
ws.data.pong?.(ws)
}
}

Expand Down
37 changes: 36 additions & 1 deletion test/extends/error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import { Elysia, t } from '../../src'

import { describe, expect, it } from 'bun:test'
import { req } from '../utils'
import { post, req } from '../utils'
import z from 'zod'

class CustomError extends Error {
constructor() {
Expand Down Expand Up @@ -90,4 +91,38 @@ describe('Error', () => {
expect(response.status).toBe(422)
expect(response.headers.get('content-type')).toBe('application/json')
})

it('validation error should handle Standard Schema with error.detail', async () => {
const sendOtpEmailSchema = z.object({
channel: z.literal('email'),
otpTo: z.email({ error: 'Must be a valid email address' })
})

const sendOtpSmsSchema = z.object({
channel: z.literal('sms'),
otpTo: z.e164({
error: 'Must be a valid phone number with country code'
})
})

const sendOtpSchema = z.discriminatedUnion('channel', [
sendOtpEmailSchema,
sendOtpSmsSchema
])

const app = new Elysia()
.onError(({ code, error, status }) => {
switch (code) {
case 'VALIDATION':
return error.detail(error.message)
}
})
.post('/', ({ body, set }) => 'ok', {
body: sendOtpSchema
})

const response = await app.handle(post('/', {}))

expect(response.status).toBe(422)
})
})
39 changes: 38 additions & 1 deletion test/ws/connection.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'bun:test'
import { Elysia } from '../../src'
import { Elysia, t } from '../../src'
import { newWebsocket, wsOpen, wsClose, wsClosed } from './utils'
import { req } from '../utils'

Expand Down Expand Up @@ -173,4 +173,41 @@ describe('WebSocket connection', () => {
name: 'Jane Doe'
})
})

it('call ping/pong', async () => {
let pinged = false
let ponged = false

const app = new Elysia()
.ws('/', {
ping() {
pinged = true
},
pong() {
ponged = true
},
async message(ws) {
}
})
.listen(0)

const ws = new WebSocket(`ws://localhost:${app.server?.port}`)

await new Promise<void>((resolve) => {
ws.addEventListener('open', () => {
ws.ping()
ws.send('df')
ws.pong()

resolve()
}, {
once: true
})
})

await Bun.sleep(3)

expect(pinged).toBe(true)
expect(ponged).toBe(true)
})
})
2 changes: 1 addition & 1 deletion test/ws/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Server } from 'bun'

export const newWebsocket = (server: Server, path = '/ws') =>
export const newWebsocket = (server: Server<any>, path = '/ws') =>
new WebSocket(`ws://${server.hostname}:${server.port}${path}`, {})

export const wsOpen = (ws: WebSocket) =>
Expand Down
Loading