Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
# 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
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

Fix duplicate link reference on line 7.

Both references point to PR #1591. The second reference should point to #1590 instead.

-- [#1591](https://github.com/elysiajs/elysia/pull/1591), [#1590](https://github.com/elysiajs/elysia/pull/1591) merge set.headers without duplicating Response
+- [#1591](https://github.com/elysiajs/elysia/pull/1591), [#1590](https://github.com/elysiajs/elysia/pull/1590) merge set.headers without duplicating Response
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- [#1591](https://github.com/elysiajs/elysia/pull/1591), [#1590](https://github.com/elysiajs/elysia/pull/1591) merge set.headers without duplicating Response
- [#1591](https://github.com/elysiajs/elysia/pull/1591), [#1590](https://github.com/elysiajs/elysia/pull/1590) merge set.headers without duplicating Response
🤖 Prompt for AI Agents
In CHANGELOG.md around line 7 the second PR link incorrectly repeats #1591;
update the second reference to point to #1590 so the line reads with distinct
links to PR #1591 and PR #1590, preserving the existing link format and
punctuation.

- 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
- handle group with empty prefix

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

const app = new Elysia({
cookie: {
domain: "\\` + console.log(c.q='pwn2') }) //"
}
})
.get('/', ({ cookie: { session } }) => 'awd')

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

const root = await app.handle(
new Request('http://localhost/', {
headers: {
Cookie: 'session=1234'
}
const app = new Elysia()
.group('', (app) => {
return app.get('/ok', () => 'Hello World')
})
)
.listen(3000)

console.log(await root.text())
type Routes = keyof typeof app['~Routes']
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
42 changes: 28 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import {
getCookieValidator,
ElysiaTypeCheck,
hasType,
resolveSchema,
resolveSchema
} from './schema'
import {
composeHandler,
Expand Down Expand Up @@ -164,7 +164,12 @@ import type {
InlineHandlerNonMacro,
Router
} from './types'
import { coercePrimitiveRoot, coerceFormData, queryCoercions, stringToStructureCoercions } from './replace-schema'
import {
coercePrimitiveRoot,
coerceFormData,
queryCoercions,
stringToStructureCoercions
} from './replace-schema'

export type AnyElysia = Elysia<any, any, any, any, any, any, any>

Expand Down Expand Up @@ -589,9 +594,16 @@ export default class Elysia<
models,
normalize,
additionalCoerce: (() => {
const resolved = resolveSchema(cloned.body, models, modules)
const resolved = resolveSchema(
cloned.body,
models,
modules
)
// Only check for Files if resolved schema is a TypeBox schema (has Kind symbol)
return (resolved && Kind in resolved && (hasType('File', resolved) || hasType('Files', resolved)))
return resolved &&
Kind in resolved &&
(hasType('File', resolved) ||
hasType('Files', resolved))
? coerceFormData()
: coercePrimitiveRoot()
})(),
Expand Down Expand Up @@ -657,9 +669,16 @@ export default class Elysia<
models,
normalize,
additionalCoerce: (() => {
const resolved = resolveSchema(cloned.body, models, modules)
const resolved = resolveSchema(
cloned.body,
models,
modules
)
// Only check for Files if resolved schema is a TypeBox schema (has Kind symbol)
return (resolved && Kind in resolved && (hasType('File', resolved) || hasType('Files', resolved)))
return resolved &&
Kind in resolved &&
(hasType('File', resolved) ||
hasType('Files', resolved))
? coerceFormData()
: coercePrimitiveRoot()
})(),
Expand Down Expand Up @@ -3822,7 +3841,7 @@ export default class Elysia<
prefix: Prefix,
run: (
group: Elysia<
JoinPath<BasePath, Prefix>,
Prefix extends '' ? BasePath : JoinPath<BasePath, Prefix>,
Singleton,
Definitions,
{
Expand Down Expand Up @@ -8153,13 +8172,8 @@ export {
type TraceStream
} from './trace'

export {
getSchemaValidator,
getResponseSchemaValidator,
} from './schema'
export {
replaceSchemaTypeFromManyOptions as replaceSchemaType
} from './replace-schema'
export { getSchemaValidator, getResponseSchemaValidator } from './schema'
export { replaceSchemaTypeFromManyOptions as replaceSchemaType } from './replace-schema'

export {
mergeHook,
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))
Comment on lines +733 to 737
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

Remove debug console.log from sucrose inference loop

Inside sucrose(), after trimming the handler body, you now log the result of isContextPassToFunction:

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

This will execute for every analyzed handler in production, producing noisy logs and adding overhead in hot paths where routing/inference is performed.

Recommend either removing this log entirely or guarding it behind an explicit debug flag / environment check so it doesn’t run in normal usage.

🤖 Prompt for AI Agents
In src/sucrose.ts around lines 733 to 737, a debug console.log prints the result
of isContextPassToFunction on every handler; remove that console.log or wrap it
with a conditional debug flag (e.g., if (process.env.DEBUG_SUCROSE === 'true') )
so it only runs during explicit debugging; keep the subsequent if check intact
and ensure no stray logging remains in the hot inference loop.

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
Loading
Loading