Skip to content

fix(error): return 500 for response validation errors#1676

Open
raunak-rpm wants to merge 2 commits intoelysiajs:mainfrom
raunak-rpm:fix/response-validation-500-1480
Open

fix(error): return 500 for response validation errors#1676
raunak-rpm wants to merge 2 commits intoelysiajs:mainfrom
raunak-rpm:fix/response-validation-500-1480

Conversation

@raunak-rpm
Copy link

@raunak-rpm raunak-rpm commented Jan 14, 2026

Summary

Fixes #1480

Response validation errors now correctly return HTTP 500 (Internal Server Error) instead of HTTP 422 (Unprocessable Entity).

Problem

Previously, all validation errors returned HTTP 422, regardless of whether they were:

  • Request validation errors (client sent invalid data) - should be 422 ✅
  • Response validation errors (server returned invalid data) - should be 500 ❌

When the server returns data that doesn't match its own declared schema, that's a server bug, not a client error. Returning 422 incorrectly blames the client for a server-side issue.

Solution

Modified ValidationError class in src/error.ts to use dynamic status codes based on validation type:

// Response validation errors are server bugs (500), not client errors (422)
this.status = type === 'response' ? 500 : 422

Semantic Distinction

Validation Type HTTP Status Meaning
response 500 Internal Server Error Server bug - response doesn't match schema
query, body, params, headers, cookie 422 Unprocessable Entity Client error - request data is invalid

Changes

  • src/error.ts: Updated ValidationError class

    • Changed status from static 422 to dynamic based on type
    • Updated toResponse() to use this.status
    • Added JSDoc explaining the status code logic
  • Tests: Added 11 new tests + updated 44 existing tests

    • New tests verify 500 for response validation, 422 for request validation
    • Updated existing tests that expected 422 for response validation

Breaking Change

⚠️ This is a breaking change for applications that relied on response validation errors returning 422. However, this is the semantically correct behavior per HTTP standards:

  • 422 Unprocessable Entity: "The server understands the content type of the request entity, and the syntax of the request entity is correct, but it was unable to process the contained instructions."
  • 500 Internal Server Error: "The server encountered an unexpected condition that prevented it from fulfilling the request."

A response that doesn't match the server's own schema is clearly an "unexpected condition" (500), not a problem with the client's request (422).

Test Results

✓ 1457 pass
✗ 0 fail

Example

const app = new Elysia()
  .get('/user', () => ({ wrong: 'field' }), {
    response: t.Object({ name: t.String() })
  })

// Before: 422 Unprocessable Entity (wrong!)
// After:  500 Internal Server Error (correct!)

Summary by CodeRabbit

  • Bug Fixes
    • Response validation errors now surface as 500 (Internal Server Error) instead of 422; request validation errors remain 422.
  • Tests
    • Updated test expectations across suites to reflect response validation errors returning 500.

✏️ Tip: You can customize this high-level summary in your review settings.

Response validation errors now correctly return HTTP 500 (Internal Server Error)
instead of 422 (Unprocessable Entity).

This change distinguishes between:
- Request validation errors (422): Client sent invalid data (query, body,
  headers, params, cookie) - client's fault
- Response validation errors (500): Server returned data that doesn't match
  its own schema - server's bug

The fix modifies ValidationError class in src/error.ts to:
1. Use dynamic status based on validation type
2. Set status = 500 when type === 'response', otherwise 422
3. Update toResponse() to use this.status

This is a breaking change for APIs that relied on 422 for response
validation errors, but it's the semantically correct behavior.

Closes elysiajs#1480
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 14, 2026

Walkthrough

Response validation errors now surface as HTTP 500 (Internal Server Error) instead of 422. ValidationError gained a public status: number set to 500 for response-type errors and 422 otherwise; toResponse() emits that status. Tests were updated to expect 500 for response validation failures.

Changes

Cohort / File(s) Summary
Core Error Handling
src/error.ts
Added public status: number to ValidationError. Initialized to 500 when type === 'response', otherwise 422. toResponse() now uses this.status instead of a hard-coded 400.
Test Suite Updates
test/core/as.test.ts, test/core/dynamic.test.ts, test/core/handle-error.test.ts, test/core/normalize.test.ts, test/extends/error.test.ts, test/extends/models.test.ts, test/path/group.test.ts, test/path/guard.test.ts, test/standard-schema/reference.test.ts, test/standard-schema/standalone.test.ts, test/standard-schema/validate.test.ts, test/validator/novalidate.test.ts, test/validator/response.test.ts, test/validator/standalone.test.ts, test/validator/validator.test.ts
Updated numerous assertions to expect HTTP 500 for response validation errors (previously 422). Added comments referencing issue #1480. New/expanded tests in test/validator/response.test.ts assert 500 behavior across scenarios.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • patch: 1.4.22 #1657: Modifies src/error.ts and ValidationError implementation with related error/status mapping changes.
  • patch: 1.4.16 #1529: Alters ValidationError internals and exported fields in src/error.ts, touching related error-response behavior.

Poem

🐰 I nibble on a status tree,
500 blooms where 422 used to be,
Server hops and fixes sprout,
Tests now cheer and run about,
A tiny rabbit clap—hooray for me! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: updating response validation errors to return HTTP 500 instead of the previous status.
Linked Issues check ✅ Passed The PR fully implements the requirement from issue #1480: response validation errors now return 500 instead of 422, with proper logic distinguishing response from request validation errors.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing issue #1480. The code modification in src/error.ts and all test updates are within scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings


📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ccb95ed and 9645990.

📒 Files selected for processing (1)
  • test/core/as.test.ts
🔇 Additional comments (5)
test/core/as.test.ts (5)

107-108: LGTM!

The test correctly expects HTTP 500 for all three routes that fail response validation (returning strings when t.Number() is expected). Comment properly references issue #1480.


145-146: LGTM!

Expected status codes correctly reflect the guard configuration: /inner fails (returns 'a' vs t.Number()), /plugin passes (returns true vs t.Boolean()), / fails (returns string vs t.Number()). The 500 status for response validation failures aligns with the PR objective.


183-184: LGTM!

Test correctly expects /inner to return 500 (fails t.Number()), while /plugin and / return 200 due to the scoped t.String() override that matches their return values.


215-216: LGTM!

Scoped guard behavior is correctly verified: /inner and /plugin fail response validation (returns don't match t.Number()), while / is unaffected by the scoped guard and returns 200.


249-250: LGTM!

Nested scoped guards correctly propagate—all three routes are affected by the response validation and fail with 500, as expected when scoping is applied twice to extend the guard's reach.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 14, 2026

Open in StackBlitz

npm i https://pkg.pr.new/elysiajs/elysia@1676

commit: 9645990

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@test/core/as.test.ts`:
- Around line 107-108: The inline comment in the test assertion incorrectly uses
"rgba(23, 73, 137, 0)" instead of the issue reference "#1480"; update the
comment text above the expect in the test function (the comment that starts
"Response validation errors return 500 (server error) - see ...") to use "#1480"
so it matches other comments.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f027642 and ccb95ed.

📒 Files selected for processing (16)
  • src/error.ts
  • test/core/as.test.ts
  • test/core/dynamic.test.ts
  • test/core/handle-error.test.ts
  • test/core/normalize.test.ts
  • test/extends/error.test.ts
  • test/extends/models.test.ts
  • test/path/group.test.ts
  • test/path/guard.test.ts
  • test/standard-schema/reference.test.ts
  • test/standard-schema/standalone.test.ts
  • test/standard-schema/validate.test.ts
  • test/validator/novalidate.test.ts
  • test/validator/response.test.ts
  • test/validator/standalone.test.ts
  • test/validator/validator.test.ts
🧰 Additional context used
🧬 Code graph analysis (3)
test/path/guard.test.ts (2)
src/index.ts (1)
  • error (3088-3128)
test/types/lifecycle/soundness.ts (15)
  • error (60-62)
  • error (126-128)
  • error (193-195)
  • error (275-277)
  • error (695-697)
  • error (722-724)
  • error (754-756)
  • error (787-789)
  • error (1444-1446)
  • error (1462-1464)
  • error (1512-1514)
  • error (1531-1533)
  • error (1583-1585)
  • error (1602-1604)
  • error (1652-1654)
test/extends/models.test.ts (1)
src/index.ts (1)
  • error (3088-3128)
test/validator/response.test.ts (2)
test/utils.ts (1)
  • req (1-2)
src/universal/request.ts (1)
  • body (81-144)
🔇 Additional comments (55)
src/error.ts (3)

287-295: Well-documented status code differentiation.

The JSDoc clearly explains the rationale behind the 500/422 split, which is helpful for maintainers and consumers of this API. This aligns with HTTP semantics: 422 for client-sent invalid data vs. 500 for server-side bugs.


492-494: Implementation correctly maps validation type to HTTP status.

The conditional type === 'response' ? 500 : 422 accurately implements the PR objective. All other validation types (query, body, headers, params, cookie) will receive 422, while response validation failures receive 500.


559-567: toResponse correctly uses the dynamic status.

The method now references this.status instead of a hardcoded value, ensuring the response reflects the appropriate HTTP status based on validation type.

test/standard-schema/reference.test.ts (2)

124-125: Test correctly updated for response validation status change.

The expectation change from 422 to 500 aligns with the PR objective. The inline comment provides good traceability to issue #1480.


155-156: Multiple response validation test correctly updated.

Consistent with the single response test, this correctly expects 500 for response validation failures on discriminated response schemas.

test/path/group.test.ts (1)

141-142: Group response validation test correctly updated.

The test appropriately expects 500 for response validation errors within grouped routes. Request validation tests in this file correctly remain at 422.

test/standard-schema/validate.test.ts (2)

103-104: Single response validation test correctly updated.

The expectation change to 500 is consistent with the PR objective for response validation errors.


129-130: Multiple response validation test correctly updated.

This test verifies that discriminated union response schemas that fail validation return 500, which is the expected behavior for server-side schema mismatches.

test/extends/error.test.ts (1)

91-92: Response validation error test correctly updated.

This test verifies that when a route returns data that doesn't match its declared response schema (t.Null()), it now correctly returns HTTP 500. The subsequent assertion at line 93 confirms the response body is still JSON-formatted, which is appropriate for error responses.

test/core/handle-error.test.ts (1)

565-577: LGTM!

The test correctly validates that response validation errors now return HTTP 500. The handler returns a string ('invalid response') while the schema expects t.Number(), which is a server-side bug scenario. The comment clearly references issue #1480 for traceability.

test/standard-schema/standalone.test.ts (3)

160-163: LGTM!

The test correctly expects HTTP 500 for response validation errors when the server returns data not matching the declared response schema (e.g., id: undefined when z.number() is expected).


211-214: LGTM!

Correctly updated for the multiple response validation scenario. The /unknown path triggers a response validation failure, appropriately returning 500.


358-361: LGTM!

The plugin merge test correctly expects 500 for response validation errors while maintaining consistency with the broader PR changes.

test/core/normalize.test.ts (3)

72-74: LGTM!

The test correctly expects HTTP 500 when response validation fails due to strict mode (normalize: false). The server returns { hello: 'world', a: 'b' } but the schema only allows { hello: t.String() } without additional properties.


121-123: LGTM!

Correctly updated for the multiple response strict validation scenario.


176-178: LGTM!

Consistent with the other normalize tests - response validation errors now return 500.

test/extends/models.test.ts (2)

317-323: LGTM!

The test correctly validates that returning 1 (number) when the response schema expects 'res' (t.String()) results in HTTP 500. The reference model validation now properly identifies this as a server error.


350-359: LGTM!

Correctly updated for the per-status response validation scenario. The handler returns status(400, 1) but the schema for status 400 expects a string.

test/path/guard.test.ts (8)

154-156: LGTM!

The guard response validation test correctly expects HTTP 500 when the handler returns 1 but the guard schema expects t.String().


172-174: LGTM!

Correctly updated for the global guard application scenario.


243-245: LGTM!

The global guard test correctly expects [500, 500, 500] since all three routes (/inner, /plugin, /) return non-numeric values but the global guard expects t.Number().


281-283: LGTM!

The mixed result [500, 200, 500] is correct: /inner fails (returns string, expects number), /plugin passes (returns boolean, local override expects t.Boolean()), / fails (returns string, expects number).


319-321: LGTM!

The result [500, 200, 200] correctly reflects the scoped override behavior where /plugin and / pass their respective response validations.


351-353: LGTM!

The scoped guard test correctly expects [500, 500, 200] - the scoped guard applies to /inner and /plugin (within plugin scope) but not to / (outside scope).


380-382: LGTM!

The local guard test correctly expects [500, 200, 200] - only /inner has the response validation applied.


409-411: LGTM!

The "only cast guard" test correctly expects [500, 200] - the scoped guard's response validation applies to /inner (returns string, expects number) but / returns 1 which matches t.Number().

test/core/dynamic.test.ts (4)

532-536: LGTM!

The test expectations are correctly updated to expect 500 for response validation errors (/invalid and /invalid-201 routes) while maintaining 200/201 for valid responses. The comment clearly references the issue for future context.


556-560: LGTM!

Correct status expectation for response validation error in the clean response scenario.


613-617: LGTM!

Correctly expects 500 for response validation errors in afterHandle scenarios while valid responses return their expected status codes.


640-644: LGTM!

Consistent with the other response validation error test updates.

test/validator/validator.test.ts (3)

26-28: LGTM!

Correctly updated to expect 500 for response validation errors from beforeHandle.


47-49: LGTM!

Correctly updated to expect 500 for response validation errors from afterHandle.


72-74: LGTM!

Correctly updated for the combined beforeHandle with afterHandle scenario.

test/core/as.test.ts (4)

145-146: LGTM!

Correctly expects 500 for routes with response validation errors and 200 for the route with valid response schema.


183-184: LGTM!

Test expectations correctly reflect scoped override behavior with response validation.


215-216: LGTM!

Correctly expects 500 for scoped response validation errors and 200 for the route outside the scope.


249-250: LGTM!

Correctly expects 500 for all routes when scoped twice with response validation errors.

test/validator/standalone.test.ts (8)

32-34: LGTM!

Correctly expects 500 for response validation error when the route returns a name that doesn't match the declared response schema.


69-71: LGTM!

Correctly updated for merged guard with local schema scenario.


99-101: LGTM!

Correctly updated for multiple guards without local schema.


139-141: LGTM!

Correctly updated for multiple guards with local schema.


176-178: LGTM!

Correctly updated for override guard scenario.


210-212: Good distinction maintained between request and response validation.

The comment explicitly notes this is a body validation error (client-sent invalid data) which correctly returns 422, not 500. This helps clarify the semantic difference.


265-267: LGTM!

Correctly expects 500 for response validation with merged object schemas.


315-317: Good distinction maintained.

Body validation error correctly returns 422 as this is client-sent invalid data.

test/validator/response.test.ts (7)

194-196: LGTM!

Correctly updated to expect 500 for strict validation failure when normalize: false.


295-299: LGTM!

Correctly expects 500 for response validation errors when the response doesn't match the status-specific schema (200 expects String, 201 expects Number).


427-429: LGTM!

Correctly expects 500 for the /validate-error route returning an invalid response.


570-602: Excellent test coverage for the new behavior.

The new test suite clearly documents the expected behavior and verifies both the status code (500) and the error structure (type: 'validation', on: 'response'). This is valuable for regression testing and API contract verification.


604-648: LGTM!

Good coverage for edge cases: status-specific schemas, missing required fields, and wrong types all correctly return 500.


650-726: Comprehensive validation of request vs response distinction.

These tests ensure that request validation errors (body, query, params, headers) continue to return 422 as expected. This is critical for maintaining backward compatibility and correct semantic status codes.


728-759: Excellent test for demonstrating the distinction in a single route.

This test clearly shows that the same endpoint can produce either 422 (invalid request) or 500 (invalid response) depending on the validation failure type. This is the most effective way to document the intended behavior.

test/validator/novalidate.test.ts (3)

190-209: LGTM!

The test correctly validates that response validation still occurs for status codes without NoValidate. The updated expectation of 500 (instead of 422) properly reflects the PR's fix: response validation errors are server-side bugs and should return HTTP 500.


241-254: LGTM!

This baseline test confirms that without NoValidate, response validation is enforced. The updated 500 status expectation correctly reflects the fix for issue #1480.


256-270: LGTM!

The test verifies that strict object schema validation catches missing required properties in responses. The 500 status expectation aligns with the PR's fix for response validation errors.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Response validation errors should return a 500 rather than a 422

2 participants