Skip to content
Merged
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
8 changes: 3 additions & 5 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ jobs:
bun-version: latest
registry-url: "https://registry.npmjs.org"

- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version: '20.x'
node-version: '24.x'
registry-url: 'https://registry.npmjs.org'

- name: Install packages
Expand All @@ -47,7 +47,5 @@ jobs:
run: bun run test:cf

- name: 'Publish'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
npm publish --provenance --access=public
NODE_AUTH_TOKEN="" npm publish --provenance --access=public
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
# 1.4.19 - 13 Dec 2025
Security:
- reject invalid cookie signature when using cookie rotation

Improvement
- [#1609](https://github.com/elysiajs/elysia/issues/1609) calling afterResponse with aot: false
- [#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
- [#1604](https://github.com/elysiajs/elysia/issues/1604) save lazy compilation of `Elysia.fetch` for up to 45x performance improvement
- [#1588](https://github.com/elysiajs/elysia/pull/1588), [#1587](https://github.com/elysiajs/elysia/pull/1587) add seen weakset during mergeDeep

Bug fix:
- [#1614](https://github.com/elysiajs/elysia/issues/1614) Cloudflare Worker with dynamic path
- add set immediate fallback
- [#1597](https://github.com/elysiajs/elysia/issues/1597) safeParse, parse to static type
- [#1596](https://github.com/elysiajs/elysia/issues/1596) handle context pass to function with sub context
- [#1591](https://github.com/elysiajs/elysia/pull/1591), [#1590](https://github.com/elysiajs/elysia/pull/1591) merge set.headers without duplicating Response
- [#1546](https://github.com/elysiajs/elysia/issues/1546) override group prefix type
- [#1526](https://github.com/elysiajs/elysia/issues/1526) default error handler doesn't work in Cloudflare Workers
- handle group with empty prefix in type-level

# 1.4.18 - 4 Dec 2025
Security:
- use `JSON.stringify` over custom escape implementation
Expand Down
3 changes: 3 additions & 0 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"memoirist": "^0.4.0",
},
"devDependencies": {
"@elysiajs/cors": "^1.4.0",
"@elysiajs/openapi": "^1.4.1",
"@types/bun": "^1.2.12",
"@types/cookie": "1.0.0",
Expand Down Expand Up @@ -55,6 +56,8 @@

"@borewit/text-codec": ["@borewit/[email protected]", "", {}, "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA=="],

"@elysiajs/cors": ["@elysiajs/[email protected]", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-pb0SCzBfFbFSYA/U40HHO7R+YrcXBJXOWgL20eSViK33ol1e20ru2/KUaZYo5IMUn63yaTJI/bQERuQ+77ND8g=="],

"@elysiajs/openapi": ["@elysiajs/[email protected]", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-d75bMxYJpN6qSDi/z9L1S7SLk1S/8Px+cTb3W2lrYzU8uQ5E0kXdy1oOMJEfTyVsz3OA19NP9KNxE7ztSbLBLg=="],

"@emnapi/core": ["@emnapi/[email protected]", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
Expand Down
29 changes: 9 additions & 20 deletions example/a.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,11 @@
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())
// This uses aot: true by default in 1.4 (broken on Bun)
const app = new Elysia({ systemRouter: true })
.get("/", "Hello Elysia")
.get("/json", () => ({ message: "Hello World", timestamp: Date.now() }))

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

console.log(await root.text())
Bun.serve({
port: 3000,
fetch: app.fetch
})
17 changes: 12 additions & 5 deletions src/adapter/bun/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,8 @@ export const BunAdapter: ElysiaAdapter = {

// @ts-expect-error private
app.promisedModules.then(async () => {
if (typeof app.config.aot) app.compile()

app.server?.reload({
...serve,
fetch: app.fetch,
Expand Down Expand Up @@ -439,11 +441,13 @@ export const BunAdapter: ElysiaAdapter = {
normalize: app.config.normalize
})

const validateMessage = messageValidator ?
messageValidator.provider === 'standard'
? (data: unknown) => messageValidator.schema['~standard'].validate(data).issues
const validateMessage = messageValidator
? messageValidator.provider === 'standard'
? (data: unknown) =>
messageValidator.schema['~standard'].validate(data)
.issues
: (data: unknown) => messageValidator.Check(data) === false
: undefined
: undefined

const responseValidator = getSchemaValidator(response as any, {
// @ts-expect-error private property
Expand Down Expand Up @@ -569,7 +573,10 @@ export const BunAdapter: ElysiaAdapter = {
) => {
const message = await parseMessage(ws, _message)

if (validateMessage && validateMessage(message)) {
if (
validateMessage &&
validateMessage(message)
) {
const validationError = new ValidationError(
'message',
messageValidator!,
Expand Down
9 changes: 5 additions & 4 deletions src/adapter/cloudflare-worker/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ElysiaAdapter } from '../..'
import { WebStandardAdapter } from '../web-standard/index'
import { composeErrorHandler } from '../../compose'

export function isCloudflareWorker() {
try {
Expand Down Expand Up @@ -62,15 +63,15 @@ export const CloudflareAdapter: ElysiaAdapter = {
}
},
beforeCompile(app) {
// @ts-ignore
app.handleError = composeErrorHandler(app)
for (const route of app.routes) route.compile()
},
listen(app) {
return (options, callback) => {
listen() {
return () => {
console.warn(
'Cloudflare Worker does not support listen method. Please export default Elysia instance instead.'
)

app.compile()
}
}
}
34 changes: 23 additions & 11 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,28 @@ 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' ||
(Kind in s && s[Kind] === '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 +1785,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 @@ -2212,7 +2224,8 @@ export const composeHandler = ({
})
console.log('---')

process.exit(1)
if (typeof process?.exit === 'function') process.exit(1)
return () => new Response('Internal Server Error', { status: 500 })
}
}

Expand Down Expand Up @@ -2287,7 +2300,6 @@ export const createHoc = (app: AnyElysia, fnName = 'map') => {
if (!hoc.length) return 'return ' + fnName

const adapter = app['~adapter'].composeGeneralHandler

let handler = fnName

for (let i = 0; i < hoc.length; i++)
Expand Down Expand Up @@ -2337,7 +2349,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 +2583,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
2 changes: 1 addition & 1 deletion src/cookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ export const parseCookie = async (

value = temp
} else {
let decoded = true
let decoded = false
for (let i = 0; i < secrets.length; i++) {
const temp = await unsignCookie(value as string, secrets[i])

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