Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# 1.4.19 - 9 Dec 2025
Improvement
- [#1588](https://github.com/elysiajs/elysia/pull/1588), [#1587](https://github.com/elysiajs/elysia/pull/1587) add seen weakset during mergeDeep
- [#1607](https://github.com/elysiajs/elysia/issues/1607), [#1606](https://github.com/elysiajs/elysia/issues/1606), [#1139](https://github.com/elysiajs/elysia/issues/1138) data coercion on nested form data
- [#1609](https://github.com/elysiajs/elysia/issues/1609) calling afterResponse with aot: false

Bug fix:
- [#1591](https://github.com/elysiajs/elysia/pull/1591), [#1590](https://github.com/elysiajs/elysia/pull/1591) merge set.headers without duplicating Response
- add set immediate fallback
- [#1596](https://github.com/elysiajs/elysia/issues/1596) handle context pass to function with sub context
- [#1597](https://github.com/elysiajs/elysia/issues/1597) safeParse, parse to static type

# 1.4.18 - 4 Dec 2025
Security:
- use `JSON.stringify` over custom escape implementation
Expand Down
27 changes: 9 additions & 18 deletions example/a.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
import { Elysia, t } from '../src'
import * as z from 'zod'
import { post, req } from '../test/utils'
import { t, getSchemaValidator } from '../src'
import { sucrose } from '../src/sucrose'

const app = new Elysia({
cookie: {
domain: "\\` + console.log(c.q='pwn2') }) //"
const v = sucrose({
handler: (context) => {
console.log('path >>> ', context.path)
console.log(context)

return { id: 'test' }
}
})
.get('/', ({ cookie: { session } }) => 'awd')

console.log(app.routes[0].compile().toString())

const root = await app.handle(
new Request('http://localhost/', {
headers: {
Cookie: 'session=1234'
}
})
)

console.log(await root.text())
console.log(v)
28 changes: 19 additions & 9 deletions src/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import {
ELYSIA_REQUEST_ID,
getLoosePath,
hasSetImmediate,
lifeCycleToFn,
randomId,
redirect,
Expand Down Expand Up @@ -444,6 +445,10 @@ const coerceTransformDecodeError = (
`throw error.error ?? new ValidationError('${type}',validator.${type},${value},${allowUnsafeValidationDetails})}` +
`}`

const setImmediateFn = hasSetImmediate
? 'setImmediate'
: 'Promise.resolve().then'

export const composeHandler = ({
app,
path,
Expand Down Expand Up @@ -804,7 +809,7 @@ export const composeHandler = ({
let afterResponse = ''

afterResponse +=
`\nsetImmediate(async()=>{` +
`\n${setImmediateFn}(async()=>{` +
`if(c.responseValue){` +
`if(c.responseValue instanceof ElysiaCustomStatusResponse) c.set.status=c.responseValue.code\n` +
(hasStream
Expand Down Expand Up @@ -1474,21 +1479,26 @@ export const composeHandler = ({
if (candidate) {
const isFirst = fileUnions.length === 0
// Handle case where schema is wrapped in a Union (e.g., ObjectString coercion)
let properties = candidate.schema?.properties ?? type.properties
let properties =
candidate.schema?.properties ?? type.properties

// If no properties but schema is a Union, try to find the Object in anyOf
if (!properties && candidate.schema?.anyOf) {
const objectSchema = candidate.schema.anyOf.find((s: any) => s.type === 'object')
const objectSchema =
candidate.schema.anyOf.find(
(s: any) => s.type === 'object'
)
if (objectSchema) {
properties = objectSchema.properties
}
}

if (!properties) continue

const iterator = Object.entries(
properties
) as [string, TSchema][]
const iterator = Object.entries(properties) as [
string,
TSchema
][]

let validator = isFirst ? '\n' : ' else '
validator += `if(fileUnions[${fileUnions.length}].Check(c.body)){`
Expand Down Expand Up @@ -1773,7 +1783,7 @@ export const composeHandler = ({
(hasTrace || hooks.afterResponse?.length
? `afterHandlerStreamListener=stream[2]\n`
: '') +
`setImmediate(async ()=>{` +
`${setImmediateFn}(async ()=>{` +
`if(listener)for await(const v of listener){}\n`
handleReporter.resolve()
fnLiteral += `})` + (maybeAsync ? '' : `})()`) + `}else{`
Expand Down Expand Up @@ -2337,7 +2347,7 @@ export const composeGeneralHandler = (app: AnyElysia) => {

const prefix = app.event.afterResponse.some(isAsync) ? 'async' : ''
afterResponse +=
`\nsetImmediate(${prefix}()=>{` +
`\n${setImmediateFn}(${prefix}()=>{` +
`if(c.responseValue instanceof ElysiaCustomStatusResponse) c.set.status=c.responseValue.code\n`

for (let i = 0; i < app.event.afterResponse.length; i++) {
Expand Down Expand Up @@ -2571,7 +2581,7 @@ export const composeErrorHandler = (app: AnyElysia) => {

let afterResponse = ''
const prefix = hooks.afterResponse?.some(isAsync) ? 'async' : ''
afterResponse += `\nsetImmediate(${prefix}()=>{`
afterResponse += `\n${setImmediateFn}(${prefix}()=>{`

const reporter = createReport({
context: 'context',
Expand Down
28 changes: 21 additions & 7 deletions src/dynamic-handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { parseQuery } from './parse-query'
import type { ElysiaTypeCheck } from './schema'
import type { TypeCheck } from './type-system'
import type { Handler, LifeCycleStore, SchemaValidator } from './types'
import { redirect, StatusMap, signCookie } from './utils'
import { hasSetImmediate, redirect, StatusMap, signCookie } from './utils'

// JIT Handler
export type DynamicHandler = {
Expand Down Expand Up @@ -79,6 +79,8 @@ export const createDynamicHandler = (app: AnyElysia) => {
response: unknown
}

let hooks: DynamicHandler['hooks']

try {
if (app.event.request)
for (let i = 0; i < app.event.request.length; i++) {
Expand Down Expand Up @@ -109,7 +111,8 @@ export const createDynamicHandler = (app: AnyElysia) => {
throw new NotFoundError()
}

const { handle, hooks, validator, content, route } = handler.store
const { handle, validator, content, route } = handler.store
hooks = handler.store.hooks

let body: string | Record<string, any> | undefined
if (request.method !== 'GET' && request.method !== 'HEAD') {
Expand Down Expand Up @@ -661,11 +664,22 @@ export const createDynamicHandler = (app: AnyElysia) => {
// @ts-expect-error private
return app.handleError(context, reportedError)
} finally {
if (app.event.afterResponse)
setImmediate(async () => {
for (const afterResponse of app.event.afterResponse!)
await afterResponse.fn(context as any)
})
const afterResponses = hooks!
? hooks.afterResponse
: app.event.afterResponse

if (afterResponses) {
if (hasSetImmediate)
setImmediate(async () => {
for (const afterResponse of afterResponses)
await afterResponse.fn(context as any)
})
else
Promise.resolve().then(async () => {
for (const afterResponse of afterResponses)
await afterResponse.fn(context as any)
})
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,7 @@ export default class Elysia<
route: path
})


const encoded = encodePath(loosePath)
if (loosePath !== encoded)
this.router.dynamic.add(method, loosePath, {
Expand Down
9 changes: 5 additions & 4 deletions src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import type {
InputSchema,
MaybeArray,
StandaloneInputSchema,
StandardSchemaV1LikeValidate
StandardSchemaV1LikeValidate,
UnwrapSchema
} from './types'

import type { StandardSchemaV1Like } from './types'
Expand All @@ -40,10 +41,10 @@ export interface ElysiaTypeCheck<T extends TSchema>
provider: 'typebox' | 'standard'
schema: T
config: Object
Clean?(v: unknown): T
parse(v: unknown): T
Clean?(v: unknown): UnwrapSchema<T>
parse(v: unknown): UnwrapSchema<T>
safeParse(v: unknown):
| { success: true; data: T; error: null }
| { success: true; data: UnwrapSchema<T>; error: null }
| {
success: false
data: null
Expand Down
38 changes: 35 additions & 3 deletions src/sucrose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,8 +551,36 @@ export const isContextPassToFunction = (
) => {
// ! Function is passed to another function, assume as all is accessed
try {
const captureFunction = new RegExp(`\\w\\((.*?)?${context}`, 'gs')
captureFunction.test(body)
const captureFunction = new RegExp(
`\\w\\((?:.*?)?${context}(?:.*?)?\\)`,
'gs'
)
const exactParameter = new RegExp(`${context}(,|\\))`, 'gs')

const length = body.length
let fn

fn = captureFunction.exec(body) + ''
while (
captureFunction.lastIndex !== 0 &&
captureFunction.lastIndex < length + (fn ? fn.length : 0)
) {
if (fn && exactParameter.test(fn)) {
inference.query = true
inference.headers = true
inference.body = true
inference.cookie = true
inference.set = true
inference.server = true
inference.url = true
inference.route = true
inference.path = true

return true
}

fn = captureFunction.exec(body) + ''
}

/*
Since JavaScript engine already format the code (removing whitespace, newline, etc.),
Expand Down Expand Up @@ -700,7 +728,11 @@ export const sucrose = (
code.charCodeAt(0) === 123 &&
code.charCodeAt(body.length - 1) === 125
)
code = code.slice(1, -1)
code = code.slice(1, -1).trim()

console.log(
isContextPassToFunction(mainParameter, code, fnInference)
)

if (!isContextPassToFunction(mainParameter, code, fnInference))
inferBodyReference(code, aliases, fnInference)
Expand Down
1 change: 1 addition & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ export const lifeCycleToArray = (a: LifeCycleStore) => {

const isBun = typeof Bun !== 'undefined'
const hasBunHash = isBun && typeof Bun.hash === 'function'
export const hasSetImmediate = typeof setImmediate === 'function'

// https://stackoverflow.com/a/52171480
export const checksum = (s: string) => {
Expand Down
103 changes: 62 additions & 41 deletions test/core/dynamic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -694,47 +694,68 @@ describe('Dynamic Mode', () => {
})
})

it('handle query array reference in multiple reference format', async () => {
const IdsModel = new Elysia().model({
name: t.Object({
name: t.Array(t.String())
})
})

const app = new Elysia({ aot: false })
.use(IdsModel)
.get('/', ({ query }) => query, {
name: 'ids'
})

const data = await app
.handle(req('/?names=rapi&names=anis'))
.then((x) => x.json())

expect(data).toEqual({
names: ['rapi', 'anis']
})
})

it('handle query array reference in multiple reference format', async () => {
const IdsModel = new Elysia().model({
name: t.Object({
name: t.Array(t.String())
})
})
it('call local afterResponse on aot: false', async () => {
let called = false

const app = new Elysia({ aot: false })
.use(IdsModel)
.get('/', ({ query }) => query, {
name: 'ids'
})

const data = await app
.handle(req('/?names=rapi&names=anis'))
.then((x) => x.json())

expect(data).toEqual({
names: ['rapi', 'anis']
})
})
.guard(
{
afterResponse: () => {
called = true
}
},
(app) => app.get('/test', () => 'afterResponse')
)
.get('/', () => 'hi')

const value = await app.handle(req('/test')).then((x) => x.text())
await Bun.sleep(6.7)

expect(value).toBe('afterResponse')
expect(called).toBeTrue()
})
Comment on lines +697 to +716
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Bun.sleep milliseconds unit documentation

💡 Result:

Bun.sleep expects milliseconds (number) — or a Date to sleep until. Example: await Bun.sleep(1000) sleeps ~1000 ms. Bun.sleepSync also uses milliseconds. [1][2][3]

Sources:
[1] Bun API — Bun.sleep.
[2] Bun runtime/utils documentation — Bun.sleep.
[3] Bun v0.5.8 release notes (sleepSync uses milliseconds).


🏁 Script executed:

cd test/core && wc -l dynamic.test.ts

Repository: elysiajs/elysia

Length of output: 78


🏁 Script executed:

sed -n '697,750p' test/core/dynamic.test.ts

Repository: elysiajs/elysia

Length of output: 1346


Make afterResponse timing in this test more robust and remove dead code

The new call local afterResponse on aot: false test correctly exercises a local afterResponse hook in dynamic mode, but it relies on await Bun.sleep(6.7) (6.7 milliseconds) as a timing buffer before asserting called === true. This is a very tight margin and can be flaky under CI load or slower environments where afterResponse scheduling may not complete within that window.

Consider either:

  • Increasing the wait to a more conservative value (e.g. 50–100ms), or
  • Refactoring to assert via a polling loop or explicit signal from the scheduler instead of a hard-coded timeout.

Additionally, the large commented-out block of duplicate "handle query array reference…" tests immediately below this test should be deleted entirely rather than left commented.

🤖 Prompt for AI Agents
In test/core/dynamic.test.ts around lines 697 to 716, the test uses a very short
fixed delay (await Bun.sleep(6.7)) before asserting the afterResponse hook was
called, which is flaky; replace that hard-coded 6.7ms wait with a more robust
approach such as increasing the delay to a conservative 50–100ms or, better,
poll/wait until the `called` flag becomes true with a short timeout to avoid
flakiness; also remove the large commented-out block of duplicate "handle query
array reference…" tests immediately below this test.


// it('handle query array reference in multiple reference format', async () => {
// const IdsModel = new Elysia().model({
// name: t.Object({
// name: t.Array(t.String())
// })
// })

// const app = new Elysia({ aot: false })
// .use(IdsModel)
// .get('/', ({ query }) => query, {
// query: 'name'
// })

// const data = await app
// .handle(req('/?names=rapi&names=anis'))
// .then((x) => x.json())

// expect(data).toEqual({
// names: ['rapi', 'anis']
// })
// })

// it('handle query array reference in multiple reference format', async () => {
// const IdsModel = new Elysia().model({
// name: t.Object({
// name: t.Array(t.String())
// })
// })

// const app = new Elysia({ aot: false })
// .use(IdsModel)
// .get('/', ({ query }) => query, {
// query: 'name'
// })

// const data = await app
// .handle(req('/?names=rapi&names=anis'))
// .then((x) => x.json())

// expect(data).toEqual({
// names: ['rapi', 'anis']
// })
// })
})
Loading
Loading