Skip to content

Commit 5472291

Browse files
committed
🔧 fix: #1453 add allowUnsafeValidationDetails for disabling unsafe validation details in production mode
1 parent 0a19175 commit 5472291

File tree

5 files changed

+85
-42
lines changed

5 files changed

+85
-42
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
# 1.4.13 - 23 Oct 2025
2+
Improvement:
3+
- [#1453](https://github.com/elysiajs/elysia/issues/1453) add `allowUnsafeValidationDetails` for disabling unsafe validation details in production mode
4+
5+
Bug fix:
26
- [#1502](https://github.com/elysiajs/elysia/issues/1502) afterHandle doesn't update status
37
- [#1495](https://github.com/elysiajs/elysia/pull/1495) request server hook parameters are typed as any (Bun 1.3.0)
48
- [#1483](https://github.com/elysiajs/elysia/pull/1483) handle standard schema validators in ValidationError.all
59
- [#1459](https://github.com/elysiajs/elysia/pull/1459) fix strictPath behavior when aot: false is set
610
- [#1455](https://github.com/elysiajs/elysia/pull/1455) graceful shutdown not awaiting server.stop
7-
- [#1499](https://github.com/elysiajs/elysia/pull/1449) fails when mergint with t.Optional schema
11+
- [#1499](https://github.com/elysiajs/elysia/pull/1449) fails when merging with t.Optional schema
12+
13+
change:
14+
- make `@types/bun` an optional dependency
815

916
# 1.4.12 - 14 Oct 2025
1017
Improvement:

example/a.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
import { Elysia } from '../src'
1+
import { Elysia, t } from '../src'
22
import { req } from '../test/utils'
33

4-
const app = new Elysia().onAfterResponse(({ set, responseValue }) => {
5-
console.log(responseValue)
6-
console.log(set.status)
4+
const app = new Elysia({
5+
allowUnsafeValidationDetails: true
76
})
8-
.listen(3000)
7+
.onError(({ error }) => {
8+
// console.log(error)
9+
})
10+
.get('/q', () => {}, {
11+
query: t.Object({
12+
a: t.String()
13+
})
14+
})
15+
.listen(3000)

src/compose.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -222,17 +222,19 @@ const composeValidationFactory = ({
222222
validator,
223223
encodeSchema = false,
224224
isStaticResponse = false,
225-
hasSanitize = false
225+
hasSanitize = false,
226+
allowUnsafeValidationDetails = false
226227
}: {
227228
injectResponse?: string
228229
normalize?: ElysiaConfig<''>['normalize']
229230
validator: SchemaValidator
230231
encodeSchema?: boolean
231232
isStaticResponse?: boolean
232233
hasSanitize?: boolean
234+
allowUnsafeValidationDetails?: boolean
233235
}) => ({
234236
validate: (type: string, value = `c.${type}`, error?: string) =>
235-
`c.set.status=422;throw new ValidationError('${type}',validator.${type},${value}${error ? ',' + error : ''})`,
237+
`c.set.status=422;throw new ValidationError('${type}',validator.${type},${value},${allowUnsafeValidationDetails}${error ? ',' + error : ''})`,
236238
response: (name = 'r') => {
237239
if (isStaticResponse || !validator.response) return ''
238240

@@ -254,7 +256,7 @@ const composeValidationFactory = ({
254256
`let vare${status}=validator.response[${status}].Check(${name})\n` +
255257
`if(vare${status} instanceof Promise)vare${status}=await vare${status}\n` +
256258
`if(vare${status}.issues)` +
257-
`throw new ValidationError('response',validator.response[${status}],${name},vare${status}.issues)\n` +
259+
`throw new ValidationError('response',validator.response[${status}],${name},${allowUnsafeValidationDetails},vare${status}.issues)\n` +
258260
`${name}=vare${status}.value\n` +
259261
`c.set.status=${status}\n` +
260262
'break\n'
@@ -313,17 +315,17 @@ const composeValidationFactory = ({
313315
clean({ ignoreTryCatch: true }) +
314316
`${name}=validator.response[${status}].Encode(${name})\n` +
315317
`}catch{` +
316-
`throw new ValidationError('response',validator.response[${status}],${name})` +
318+
`throw new ValidationError('response',validator.response[${status}],${name},${allowUnsafeValidationDetails})` +
317319
`}`
318-
: `throw new ValidationError('response',validator.response[${status}],${name})`) +
320+
: `throw new ValidationError('response',validator.response[${status}],${name}),${allowUnsafeValidationDetails}`) +
319321
`}`
320322
} else {
321323
if (!appliedCleaner) code += clean()
322324

323325
if (!noValidate)
324326
code +=
325327
`if(validator.response[${status}].Check(${name})===false)` +
326-
`throw new ValidationError('response',validator.response[${status}],${name})\n` +
328+
`throw new ValidationError('response',validator.response[${status}],${name},${allowUnsafeValidationDetails})\n` +
327329
`c.set.status=${status}\n`
328330
}
329331

@@ -411,12 +413,13 @@ const isGenerator = (v: Function | HookContainer) => {
411413
const coerceTransformDecodeError = (
412414
fnLiteral: string,
413415
type: string,
416+
allowUnsafeValidationDetails = false,
414417
value = `c.${type}`
415418
) =>
416419
`try{${fnLiteral}}catch(error){` +
417420
`if(error.constructor.name === 'TransformDecodeError'){` +
418421
`c.set.status=422\n` +
419-
`throw error.error ?? new ValidationError('${type}',validator.${type},${value})}` +
422+
`throw error.error ?? new ValidationError('${type}',validator.${type},${value},${allowUnsafeValidationDetails})}` +
420423
`}`
421424

422425
export const composeHandler = ({
@@ -590,13 +593,15 @@ export const composeHandler = ({
590593

591594
const normalize = app.config.normalize
592595
const encodeSchema = app.config.encodeSchema
596+
const allowUnsafeValidationDetails = app.config.allowUnsafeValidationDetails
593597

594598
const validation = composeValidationFactory({
595599
normalize,
596600
validator,
597601
encodeSchema,
598602
isStaticResponse: handler instanceof Response,
599-
hasSanitize: !!app.config.sanitize
603+
hasSanitize: !!app.config.sanitize,
604+
allowUnsafeValidationDetails
600605
})
601606

602607
if (hasHeaders) fnLiteral += adapter.headers
@@ -763,11 +768,14 @@ export const composeHandler = ({
763768
if (!hooks.afterResponse?.length && !hasTrace) return ''
764769

765770
let afterResponse = ''
766-
const prefix = hooks.afterResponse?.some(isAsync) ? 'async ' : ''
767771

768772
afterResponse +=
769-
`\nsetImmediate(${prefix}()=>{` +
770-
`if(c.responseValue instanceof ElysiaCustomStatusResponse) c.set.status=c.responseValue.code\n`
773+
`\nsetImmediate(async()=>{` +
774+
`if(c.responseValue){` +
775+
`if(c.responseValue instanceof ElysiaCustomStatusResponse) c.set.status=c.responseValue.code\n` +
776+
`else if(c.responseValue[Symbol.iterator]) for (const v of c.responseValue) { }` +
777+
`else if(c.responseValue[Symbol.asyncIterator]) for await (const v of c.responseValue) { }` +
778+
`}`
771779

772780
const reporter = createReport({
773781
trace: hooks.trace,
@@ -1184,7 +1192,8 @@ export const composeHandler = ({
11841192
if (validator.headers.hasTransform)
11851193
fnLiteral += coerceTransformDecodeError(
11861194
`c.headers=validator.headers.Decode(c.headers)\n`,
1187-
'headers'
1195+
'headers',
1196+
allowUnsafeValidationDetails
11881197
)
11891198

11901199
if (validator.headers.isOptional) fnLiteral += '}'
@@ -1226,7 +1235,8 @@ export const composeHandler = ({
12261235
if (validator.params.hasTransform)
12271236
fnLiteral += coerceTransformDecodeError(
12281237
`c.params=validator.params.Decode(c.params)\n`,
1229-
'params'
1238+
'params',
1239+
allowUnsafeValidationDetails
12301240
)
12311241
}
12321242

@@ -1279,11 +1289,13 @@ export const composeHandler = ({
12791289
// For query, we decode it twice to ensure that it works
12801290
fnLiteral += coerceTransformDecodeError(
12811291
`c.query=validator.query.Decode(c.query)\n`,
1282-
'query'
1292+
'query',
1293+
allowUnsafeValidationDetails
12831294
)
12841295
fnLiteral += coerceTransformDecodeError(
12851296
`c.query=validator.query.Decode(c.query)\n`,
1286-
'query'
1297+
'query',
1298+
allowUnsafeValidationDetails
12871299
)
12881300
}
12891301

@@ -1398,7 +1410,8 @@ export const composeHandler = ({
13981410
if (validator.body.hasTransform)
13991411
fnLiteral += coerceTransformDecodeError(
14001412
`if(isNotEmptyObject)c.body=validator.body.Decode(c.body)\n`,
1401-
'body'
1413+
'body',
1414+
allowUnsafeValidationDetails
14021415
)
14031416

14041417
if (hasUnion && validator.body.schema.anyOf?.length) {
@@ -1537,7 +1550,8 @@ export const composeHandler = ({
15371550
`for(const [key,value] of Object.entries(validator.cookie.Decode(cookieValue))){` +
15381551
`c.cookie[key].value=value` +
15391552
`}`,
1540-
'cookie'
1553+
'cookie',
1554+
allowUnsafeValidationDetails
15411555
)
15421556
}
15431557

src/error.ts

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,7 @@ const emptyHttpStatus = {
4242
export class ElysiaCustomStatusResponse<
4343
const in out Code extends number | keyof StatusMap,
4444
// no in out here so the response can be sub type of return type
45-
T = Code extends keyof InvertedStatusMap
46-
? InvertedStatusMap[Code]
47-
: Code,
45+
T = Code extends keyof InvertedStatusMap ? InvertedStatusMap[Code] : Code,
4846
const in out Status extends Code extends keyof StatusMap
4947
? StatusMap[Code]
5048
: Code = Code extends keyof StatusMap ? StatusMap[Code] : Code
@@ -270,7 +268,8 @@ export class ValidationError extends Error {
270268
| ElysiaTypeCheck<any>
271269
| StandardSchemaV1Like,
272270
public value: unknown,
273-
errors?: ValueErrorIterator
271+
private allowUnsafeValidationDetails = false,
272+
errors?: ValueErrorIterator,
274273
) {
275274
let message = ''
276275
let error
@@ -335,7 +334,7 @@ export class ValidationError extends Error {
335334
// @ts-ignore private field
336335
const schema = validator?.schema ?? validator
337336

338-
if (!isProduction) {
337+
if (!isProduction && !allowUnsafeValidationDetails) {
339338
try {
340339
expected = Value.Create(schema)
341340
} catch (error) {
@@ -352,7 +351,7 @@ export class ValidationError extends Error {
352351
error?.schema?.message || error?.schema?.error !== undefined
353352
? typeof error.schema.error === 'function'
354353
? error.schema.error(
355-
isProduction
354+
isProduction && !allowUnsafeValidationDetails
356355
? {
357356
type: 'validation',
358357
on: type,
@@ -392,7 +391,7 @@ export class ValidationError extends Error {
392391
typeof customError === 'object'
393392
? JSON.stringify(customError)
394393
: customError + ''
395-
} else if (isProduction) {
394+
} else if (isProduction && !allowUnsafeValidationDetails) {
396395
message = JSON.stringify({
397396
type: 'validation',
398397
on: type,
@@ -438,23 +437,29 @@ export class ValidationError extends Error {
438437
this.validator?.provider === 'standard' ||
439438
'~standard' in this.validator ||
440439
// @ts-ignore
441-
('schema' in this.validator && this.validator.schema && '~standard' in this.validator.schema)
440+
('schema' in this.validator &&
441+
this.validator.schema &&
442+
'~standard' in this.validator.schema)
442443
) {
443444
const standard = // @ts-ignore
444-
('~standard' in this.validator
445-
? this.validator
446-
: // @ts-ignore
447-
this.validator.schema)['~standard']
445+
(
446+
'~standard' in this.validator
447+
? this.validator
448+
: // @ts-ignore
449+
this.validator.schema
450+
)['~standard']
448451

449452
const issues = standard.validate(this.value).issues
450453

451454
// Map standard schema issues to the expected format
452-
return issues?.map((issue: any) => ({
453-
summary: issue.message,
454-
path: issue.path?.join('.') || 'root',
455-
message: issue.message,
456-
value: this.value
457-
})) || []
455+
return (
456+
issues?.map((issue: any) => ({
457+
summary: issue.message,
458+
path: issue.path?.join('.') || 'root',
459+
message: issue.message,
460+
value: this.value
461+
})) || []
462+
)
458463
}
459464

460465
// Handle TypeBox validators
@@ -521,7 +526,7 @@ export class ValidationError extends Error {
521526
const expected = this.expected
522527
const errors = this.all
523528

524-
return isProduction
529+
return isProduction && !this.allowUnsafeValidationDetails
525530
? {
526531
type: 'validation',
527532
on: this.type,

src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,16 @@ export interface ElysiaConfig<Prefix extends string | undefined> {
238238
* Sucrose (Static Code Analysis) configuration
239239
*/
240240
sucrose?: Sucrose.Settings
241+
242+
/**
243+
* Allow unsafe validation details in errors thrown by Elysia's schema validator (422 status code)
244+
*
245+
* Ideally, this should only be used in development environment or public APIs
246+
* This may leak sensitive information about the server implementation and should be used with caution in production environments.
247+
*
248+
* @default false
249+
*/
250+
allowUnsafeValidationDetails?: boolean
241251
}
242252

243253
export interface ValidatorLayer {

0 commit comments

Comments
 (0)