mirror of
https://github.com/elysiajs/elysia
synced 2026-08-09 12:35:48 +00:00
Merge branch 'main' into fix/async-generator-headers-1677
This commit is contained in:
@@ -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
@@ -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:!
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user