Merge branch 'main' into fix/async-generator-headers-1677

This commit is contained in:
SaltyAom
2026-02-09 10:56:18 +07:00
committed by GitHub
17 changed files with 392 additions and 150 deletions
+11
View File
@@ -1,3 +1,14 @@
# 1.4.23
Bug fix:
- [#1721](https://github.com/elysiajs/elysia/issues/1721) Promise<Response> with response schema
- [#1700](https://github.com/elysiajs/elysia/issues/1700) distinct union object
- [#1683](https://github.com/elysiajs/elysia/pull/1683) response validation returns 500 instead of 422 for nested schemas in dynamic mode
- [#1595](https://github.com/elysiajs/elysia/pull/1595) stream reference should point to teed value
- fix can't modify immutable headers error
Change:
- update exact-mirror to 0.2.7
# 1.4.22 - 14 Jan 2026
Improvement:
- use imperative check for `replaceURLPath` instead of allocating `new URL`
+25 -26
View File
@@ -1,6 +1,6 @@
# Welcome to Elysia.js contributing guide
# Welcome to Elysia contributing guide
Thank you for investing your time in contributing to Elysia.js! Any contribution you make will be amazing :sparkles:.
Thank you for investing your time in contributing to Elysia! Any contribution you make will be amazing :sparkles:.
Read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectable.
@@ -8,42 +8,41 @@ In this guide you will get an overview of the contribution workflow from opening
## Setup Local Development Environment
The Elysia.js repo is using [bun](https://bun.sh). Make sure you have the [latest version of bun](https://github.com/oven-sh/bun/releases) installed in your system. To run Elysia.js locally:
Elysia test cases are using [bun](https://bun.sh). Make sure you have the [latest version of bun](https://github.com/oven-sh/bun/releases) installed in your system.
To run Elysia locally:
1. Clone this repository
2. In the root of this project, run `bun install` to install all of the necessary dependencies
3. To run the development version, run `bun run dev`
2. run `bun install` in project's root
3. Run development with `bun run dev`
### Unit Testing
In Elysia.js, all of the test files are located inside the [`test/`](test/) directory. Unit testing are powered by [bun's test](https://github.com/oven-sh/bun/tree/main/packages/bun-internal-test).
All of the test files are located inside the [`test/`](test/) directory. Unit testing are powered by [bun's test](https://github.com/oven-sh/bun/tree/main/packages/bun-internal-test).
- `bun test` to run all the test inside the [`test/`](test/) directory
- `bun test test/<test-file>.ts` to run a specific test
- `bun test` to run all the test inside the [`test/`](test/) directory
- `bun test test/<test-file>.ts` to run a specific test
## Pull Request Guidelines
- Checkout a topic branch from a base branch (e.g. `main`), and merge back against that branch.
Recommended to use `main` branch as a base to work on.
- If adding a new feature:
#### General Recommendation
- Please kindly verify that you have run test suite before request a review from maintainers with `bun run test`
- We do not condone the usage of any form of plagiarism or copying code without proper attribution.
- We do not tolerate disrespectful or inappropriate behavior within the community.
- AI generated pull request without human interaction, review and supervision may result in close without further notice or ban from future contribution to Elysia.
- Add accompanying test case if possible.
#### Adding New Features
- Provide a reason why you would like to add this feature. Ideally before creating a PR, create a new issue with, explain the reason, tag as `feature request` and tag maintainer eg. "saltyaom"
- It's recommended to add test cases to cover core feature of the feature you intent to add
- Provide a convincing reason to add this feature. Ideally, you should open a suggestion issue first, and have it approved before working on it.
- If fixing a bug:
- If you are resolving a special issue, please add the issues number in the PR's description.
- Provide a detailed description of the bug in the PR. Live demo preferred.
- Add appropriate test coverage if applicable.
- It's OK to have multiple small commits as you work on the PR. GitHub can automatically squash them before merging.
#### Fixing Bug
- When opening an pull request fixing existing issue, please kindly include the issue link or id in the description
- Provide a detailed description of the bug in the PR. Live demo preferred.
- Add appropriate test coverage if applicable.
- It's OK to have multiple small commits as you work on the PR. GitHub can automatically squash them before merging.
## Thanks :purple_heart:
Thanks for all your contributions and efforts towards improving Elysia.js. We thank you for being part of our :sparkles: community :sparkles:!
Thanks for all your contributions and efforts towards improving Elysia. We thank you for being part of our community :sparkles:!
+3 -3
View File
@@ -6,7 +6,7 @@
"name": "elysia",
"dependencies": {
"cookie": "^1.1.1",
"exact-mirror": "^0.2.6",
"exact-mirror": "^0.2.7",
"fast-decode-uri-component": "^1.0.1",
"memoirist": "^0.4.0",
},
@@ -34,7 +34,7 @@
"peerDependencies": {
"@sinclair/typebox": ">= 0.34.0 < 1",
"@types/bun": ">= 1.2.0",
"exact-mirror": ">= 0.0.9",
"exact-mirror": "^0.2.6",
"file-type": ">= 20.0.0",
"openapi-types": ">= 12.0.0",
"typescript": ">= 5.0.0",
@@ -363,7 +363,7 @@
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"exact-mirror": ["exact-mirror@0.2.6", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-7s059UIx9/tnOKSySzUk5cPGkoILhTE4p6ncf6uIPaQ+9aRBQzQjc9+q85l51+oZ+P6aBxh084pD0CzBQPcFUA=="],
"exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="],
"expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="],
+23 -17
View File
@@ -1,18 +1,24 @@
import { Elysia } from '../src'
import { Elysia, status } from '../src'
import { req } from '../test/utils'
new Elysia()
.post(
"/create-post",
async (ctx) => {
return {
message: "Post Created",
data: ctx.body,
};
},
{
body: t.Object({
name: t.String(),
content: t.String(),
safeAge: t.Number(),
}),
},
const value = { message: 'meow!' }
const app = new Elysia().get('/', ({ set }) => {
set.headers.hello = 'world'
return new Response('data: hello\n\ndata: world\n\n', {
headers: {
'content-type': 'text/event-stream',
'transfer-encoding': 'chunked'
},
status: 200
})
})
const response = await app
.handle(new Request('http://localhost/'))
.then((r) => r.text())
// Should NOT double-wrap with "data: data:"
console.log(response)
// expect(response).not.toContain('data: data:')
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "elysia",
"description": "Ergonomic Framework for Human",
"version": "1.4.22",
"version": "1.4.23",
"author": {
"name": "saltyAom",
"url": "https://github.com/SaltyAom",
@@ -191,7 +191,7 @@
},
"dependencies": {
"cookie": "^1.1.1",
"exact-mirror": "^0.2.6",
"exact-mirror": "^0.2.7",
"fast-decode-uri-component": "^1.0.1",
"memoirist": "^0.4.0"
},
+5 -1
View File
@@ -14,7 +14,11 @@ export const createNativeStaticHandler = (
if (isHTMLBundle(handle)) return () => handle as any
const response = mapResponse(
handle,
handle instanceof Response
? handle.clone()
: handle instanceof Promise
? handle.then((x) => (x instanceof Response ? x.clone() : x))
: handle,
set ?? {
headers: {}
}
+60 -66
View File
@@ -175,14 +175,12 @@ export const createStreamHandler =
// Check if stream is from a pre-formatted Response body
const isSSE =
!skipFormat &&
(
// @ts-ignore First SSE result is wrapped with sse()
init?.value?.sse ??
// @ts-ignore First SSE result is wrapped with sse()
(init?.value?.sse ??
// @ts-ignore ReadableStream is wrapped with sse()
generator?.sse ??
// User explicitly set content-type to SSE
set?.headers['content-type']?.startsWith('text/event-stream')
)
set?.headers['content-type']?.startsWith('text/event-stream'))
const format = isSSE
? (data: string) => `data: ${data}\n\n`
@@ -337,79 +335,75 @@ export const handleSet = (set: Context['set']) => {
}
}
// Merge header by allocating a new one
// In Bun, response.headers can be mutable
// while in Node and Cloudflare Worker is not
// default to creating a new one instead
export function mergeHeaders(
responseHeaders: Headers,
setHeaders: Context['set']['headers']
) {
const headers = new Headers(
hasHeaderShorthand
? // @ts-ignore
responseHeaders.toJSON()
: Object.fromEntries(responseHeaders.entries())
)
// Merge headers: Response headers take precedence, set.headers fill in non-conflicting ones
if (setHeaders instanceof Headers)
for (const key of setHeaders.keys()) {
if (key === 'set-cookie') {
if (headers.has('set-cookie')) continue
for (const cookie of setHeaders.getSetCookie())
headers.append('set-cookie', cookie)
} else if (!responseHeaders.has(key))
headers.set(key, setHeaders?.get(key) ?? '')
}
else
for (const key in setHeaders)
if (key === 'set-cookie')
headers.append(key, setHeaders[key] as any)
else if (!responseHeaders.has(key))
headers.set(key, setHeaders[key] as any)
return headers
}
export function mergeStatus(
responseStatus: number,
setStatus: Context['set']['status']
) {
if (typeof setStatus === 'string') setStatus = StatusMap[setStatus]
if (responseStatus === 200) return setStatus
return responseStatus
}
export const createResponseHandler = (handler: CreateHandlerParameter) => {
const handleStream = createStreamHandler(handler)
return (response: Response, set: Context['set'], request?: Request) => {
let isCookieSet = false
// Merge headers: Response headers take precedence, set.headers fill in non-conflicting ones
if (set.headers instanceof Headers)
for (const key of set.headers.keys()) {
if (key === 'set-cookie') {
if (isCookieSet) continue
isCookieSet = true
for (const cookie of set.headers.getSetCookie())
response.headers.append('set-cookie', cookie)
} else if (!response.headers.has(key))
response.headers.set(key, set.headers?.get(key) ?? '')
}
else
for (const key in set.headers)
if (key === 'set-cookie')
(response as Response).headers.append(
key,
set.headers[key] as any
)
else if (!response.headers.has(key))
(response as Response).headers.set(
key,
set.headers[key] as any
)
const status = set.status ?? 200
const newResponse = new Response(response.body, {
headers: mergeHeaders(response.headers, set.headers),
status: mergeStatus(response.status, set.status)
})
if (
(response as Response).status !== status &&
status !== 200 &&
((response.status as number) <= 300 ||
(response.status as number) > 400)
) {
const newResponse = new Response(response.body, {
headers: response.headers,
status: set.status as number
})
if (
!(newResponse as Response).headers.has('content-length') &&
(newResponse as Response).headers.get('transfer-encoding') ===
'chunked'
)
return handleStream(
streamResponse(newResponse as Response),
responseToSetHeaders(newResponse as Response, set),
request,
true // skipFormat: don't auto-format SSE for pre-formatted Response
) as any
return newResponse
}
if (
!(response as Response).headers.has('content-length') &&
(response as Response).headers.get('transfer-encoding') ===
!(newResponse as Response).headers.has('content-length') &&
(newResponse as Response).headers.get('transfer-encoding') ===
'chunked'
)
return handleStream(
streamResponse(response as Response),
responseToSetHeaders(response as Response, set),
streamResponse(newResponse as Response),
responseToSetHeaders(newResponse as Response, set),
request,
true // skipFormat: don't auto-format SSE for pre-formatted Response
true // don't auto-format SSE for pre-formatted Response
) as any
return response
return newResponse
}
}
+3
View File
@@ -1809,6 +1809,9 @@ export const composeHandler = ({
(maybeAsync ? '' : `(async()=>{`) +
`const stream=await tee(r,3)\n` +
`r=stream[0]\n` +
(hooks.afterHandle?.length
? `c.response=c.responseValue=r\n`
: '') +
`const listener=stream[1]\n` +
(hasTrace || hooks.afterResponse?.length
? `afterHandlerStreamListener=stream[2]\n`
+35 -11
View File
@@ -530,15 +530,25 @@ export const createDynamicHandler = (app: AnyElysia) => {
if (responseValidator?.Check(response) === false) {
if (responseValidator?.Clean) {
const temp = responseValidator.Clean(response)
if (responseValidator?.Check(temp) === false)
try {
const temp = responseValidator.Clean(response)
if (responseValidator?.Check(temp) === false)
throw new ValidationError(
'response',
responseValidator,
response
)
response = temp
} catch (error) {
if (error instanceof ValidationError) throw error
throw new ValidationError(
'response',
responseValidator,
response
)
response = temp
}
} else
throw new ValidationError(
'response',
@@ -551,7 +561,9 @@ export const createDynamicHandler = (app: AnyElysia) => {
response = responseValidator.Encode(response)
if (responseValidator?.Clean)
response = responseValidator.Clean(response)
try {
response = responseValidator.Clean(response)
} catch {}
} else {
;(
context as Context & {
@@ -589,15 +601,25 @@ export const createDynamicHandler = (app: AnyElysia) => {
if (responseValidator?.Check(response) === false) {
if (responseValidator?.Clean) {
const temp = responseValidator.Clean(response)
if (responseValidator?.Check(temp) === false)
try {
const temp = responseValidator.Clean(response)
if (responseValidator?.Check(temp) === false)
throw new ValidationError(
'response',
responseValidator,
response
)
response = temp
} catch (error) {
if (error instanceof ValidationError) throw error
throw new ValidationError(
'response',
responseValidator,
response
)
response = temp
}
} else
throw new ValidationError(
'response',
@@ -611,8 +633,10 @@ export const createDynamicHandler = (app: AnyElysia) => {
responseValidator.Encode(response)
if (responseValidator?.Clean)
context.response = response =
responseValidator.Clean(response)
try {
context.response = response =
responseValidator.Clean(response)
} catch {}
const result = mapEarlyResponse(response, context.set)
// @ts-expect-error
+11 -10
View File
@@ -8,7 +8,7 @@ import type {
import { StatusMap, InvertedStatusMap } from './utils'
import type { ElysiaTypeCheck } from './schema'
import { Prettify, StandardSchemaV1Like } from './types'
import { StandardSchemaV1Like } from './types'
// ? Cloudflare worker support
const env =
@@ -160,7 +160,9 @@ interface ValueErrorWithSummary extends ValueError {
summary?: string
}
export const mapValueError = (error: ValueError): ValueErrorWithSummary => {
export const mapValueError = (
error: ValueError | undefined
): ValueErrorWithSummary | undefined => {
if (!error) return error
let { message, path, value, type } = error
@@ -388,7 +390,7 @@ export class ValidationError extends Error {
// @ts-ignore private field
const schema = validator?.schema ?? validator
if (!isProduction && !allowUnsafeValidationDetails) {
if (!isProduction && !allowUnsafeValidationDetails)
try {
expected = Value.Create(schema)
} catch (error) {
@@ -399,7 +401,6 @@ export class ValidationError extends Error {
error
}
}
}
customError =
error?.schema?.message || error?.schema?.error !== undefined
@@ -418,7 +419,7 @@ export class ValidationError extends Error {
property: accessor,
message: error?.message,
summary:
mapValueError(error).summary,
mapValueError(error)?.summary,
found: value,
expected,
errors:
@@ -458,7 +459,7 @@ export class ValidationError extends Error {
on: type,
property: accessor,
message: error?.message,
summary: mapValueError(error).summary,
summary: mapValueError(error)?.summary,
expected,
found: value,
errors:
@@ -520,7 +521,9 @@ export class ValidationError extends Error {
// Handle TypeBox validators
return 'Errors' in this.validator
? [...this.validator.Errors(this.value)].map(mapValueError)
? [...this.validator.Errors(this.value)]
.filter((x) => x)
.map((x) => mapValueError(x) as ValueErrorWithSummary)
: // @ts-ignore
[...Value.Errors(this.validator, this.value)].map(mapValueError)
}
@@ -596,9 +599,7 @@ export class ValidationError extends Error {
on: this.type,
property: this.valueError?.path || 'root',
message,
summary: this.valueError
? mapValueError(this.valueError).summary
: undefined,
summary: mapValueError(this.valueError)?.summary,
found: value,
expected,
errors
+4 -4
View File
@@ -1197,14 +1197,14 @@ export type InlineHandler<
resolve: {}
}
> =
| InlineResponse
| MaybePromise<InlineResponse>
| ((
context: Context<
Route & MacroContext,
Singleton & { resolve: MacroContext['resolve'] }
>
) =>
| Response
| MaybePromise<Response>
| MaybePromise<
{} extends Route['response']
? unknown
@@ -1243,9 +1243,9 @@ export type InlineHandlerNonMacro<
resolve: {}
}
> =
| InlineResponse
| MaybePromise<InlineResponse>
| ((context: Context<Route, Singleton>) =>
| Response
| MaybePromise<Response>
| MaybePromise<
{} extends Route['response']
? unknown
+2 -5
View File
@@ -5,7 +5,6 @@ import type {
LifeCycleStore,
MaybeArray,
InputSchema,
BaseMacro,
LifeCycleType,
HookContainer,
GracefulHandler,
@@ -24,8 +23,7 @@ import type {
RouteSchema
} from './types'
import { ElysiaFile } from './universal/file'
export const hasHeaderShorthand = 'toJSON' in new Headers()
import { isBun } from './universal/utils'
export const replaceUrlPath = (url: string, pathname: string) => {
const pathStartIndex = url.indexOf('/', 11)
@@ -401,8 +399,7 @@ export const lifeCycleToArray = (a: LifeCycleStore) => {
return a
}
const isBun = typeof Bun !== 'undefined'
const hasBunHash = isBun && typeof Bun.hash === 'function'
export const hasHeaderShorthand = isBun ? 'toJSON' in new Headers() : false
export const hasSetImmediate = typeof setImmediate === 'function'
// https://stackoverflow.com/a/52171480
+2 -2
View File
@@ -279,8 +279,8 @@ describe('Handle Error', () => {
const value = { message: 'meow!' }
const response: Response = await new Elysia()
.mapResponse(({ response }) => {
if (typeof response === 'object')
.mapResponse(({ responseValue }) => {
if (typeof responseValue === 'object')
return new Response('Don Quixote', {
headers: {
'content-type': 'text/plain'
+34 -1
View File
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'bun:test'
import { describe, expect, it } from 'bun:test'
import { req } from '../utils'
import { Elysia, sse } from '../../src'
@@ -604,5 +604,38 @@ describe('Stream', () => {
expect(onErrorCalled).toBe(true)
expect(errorCode).toBe(500)
expect(response.headers.get('x-custom-header')).toBe('test-value')
it('handle sse with plugin global hooks and trace', async () => {
const PluginA = () =>
new Elysia({ name: 'PluginA' })
.onBeforeHandle(() => {})
.onAfterHandle(() => {})
.onParse(() => {})
.onTransform(() => {})
.onError(() => {})
.onAfterResponse(() => {})
.onStart(() => {})
.onStop(() => {})
.onRequest(() => {})
.trace(() => {})
.as('global')
const app = new Elysia().use(PluginA()).get('/sse', async function* () {
yield sse({ event: 'message', data: { meow: '1' } })
yield sse({ event: 'message', data: { meow: '2' } })
yield sse({ event: 'message', data: { meow: '3' } })
})
const response = await app.handle(req('/sse'))
expect(response.headers.get('content-type')).toBe('text/event-stream')
const result = []
for await (const chunk of streamResponse(response)) result.push(chunk)
expect(result).toHaveLength(3)
expect(result).toEqual([
'event: message\ndata: {"meow":"1"}\n\n',
'event: message\ndata: {"meow":"2"}\n\n',
'event: message\ndata: {"meow":"3"}\n\n'
])
})
})
+16 -1
View File
@@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { expectTypeOf } from 'expect-type'
import {
type Cookie,
Elysia,
@@ -12,6 +11,8 @@ import {
t
} from '../../src'
import { expectTypeOf } from 'expect-type'
const app = new Elysia()
// ? default value of context
@@ -2969,3 +2970,17 @@ type a = keyof {}
}>()
})
}
// ? Promise<Response>
{
async function handler() {
return new Response(JSON.stringify({ text: 'hello' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
}
new Elysia().get('/hello', () => handler(), {
response: { 200: t.Object({ text: t.String() }) }
})
}
@@ -0,0 +1,135 @@
import { describe, expect, it } from 'bun:test'
import { Elysia, t } from '../../src'
// Issue #1659: Response validation with nested schemas crashes with 500 instead of 422
// https://github.com/elysiajs/elysia/issues/1659
//
// Root cause: exact-mirror's Clean() function assumes valid data structure
// and throws when accessing nested properties on null values.
// Fix: Wrap Clean() calls in try-catch in dynamic-handle.ts
describe('Response validation nested schemas', () => {
it('should return 422 for invalid nested response (aot: false)', async () => {
const app = new Elysia({ aot: false }).post(
'/test',
// @ts-expect-error - intentionally returning invalid data to test validation
() => ({
items: [
['t1', { file: { ver: { s: '', m: null } } }],
['t2', { file: { ver: null } }] // Invalid - ver should be object
]
}),
{
body: t.Object({}),
response: t.Object({
items: t.Array(
t.Tuple([
t.String(),
t.Union([
t.Object({
file: t.Object({
ver: t.Object({
s: t.String(),
m: t.Nullable(t.String())
})
})
})
])
])
)
})
}
)
const res = await app.handle(
new Request('http://localhost/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{}'
})
)
// Should be 422 (validation error), not 500 (internal error)
expect(res.status).toBe(422)
const json = (await res.json()) as { type: string; errors?: unknown[] }
expect(json.type).toBe('validation')
expect(json.errors?.length).toBeGreaterThan(0)
})
it('should return 422 for invalid nested response (aot: true)', async () => {
const app = new Elysia({ aot: true }).post(
'/test',
// @ts-expect-error - intentionally returning invalid data to test validation
() => ({
items: [
['t1', { file: { ver: { s: '', m: null } } }],
['t2', { file: { ver: null } }] // Invalid
]
}),
{
body: t.Object({}),
response: t.Object({
items: t.Array(
t.Tuple([
t.String(),
t.Union([
t.Object({
file: t.Object({
ver: t.Object({
s: t.String(),
m: t.Nullable(t.String())
})
})
})
])
])
)
})
}
)
const res = await app.handle(
new Request('http://localhost/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{}'
})
)
expect(res.status).toBe(422)
const json = (await res.json()) as { type: string; errors?: unknown[] }
expect(json.type).toBe('validation')
expect(json.errors?.length).toBeGreaterThan(0)
})
it('should return 422 for tuple with null nested object (aot: false)', async () => {
const app = new Elysia({ aot: false }).get(
'/test',
// @ts-expect-error - intentionally returning invalid data to test validation
() => ({
data: ['id', { nested: null }] // nested should be object with 'value'
}),
{
response: t.Object({
data: t.Tuple([
t.String(),
t.Object({
nested: t.Object({
value: t.String()
})
})
])
})
}
)
const res = await app.handle(new Request('http://localhost/test'))
expect(res.status).toBe(422)
const json = (await res.json()) as { type: string }
expect(json.type).toBe('validation')
})
})
+21 -1
View File
@@ -370,7 +370,7 @@ describe('Response Validator', () => {
})
it('return static undefined with status based schema', async () => {
const app = new Elysia().get('/', undefined, {
const app = new Elysia().get('/', undefined as any, {
response: {
200: t.Union([
t.Void(),
@@ -562,4 +562,24 @@ describe('Response Validator', () => {
expect(result.join('')).toContain('data: {"name":"Name"}')
})
it('handle distinct union', () => {
const app = new Elysia()
.get('/health', () => ({ status: 'healthy' }) as const, {
response: {
200: t.Union([
t.Object({
status: t.Literal('a'),
a: t.Object({ b: t.Integer() })
}),
t.Object({ status: t.Literal('healthy') })
])
}
})
.listen(3000)
const status = app.handle(req('/health')).then((x) => x.status)
expect(status).resolves.toBe(200)
})
})