Skip to content

Conversation

@MarcelOlsen
Copy link
Contributor

@MarcelOlsen MarcelOlsen commented Nov 15, 2025

This resolves #1547

Problem

Set-Cookie headers are not sent to clients when errors are thrown in route handlers, breaking critical use cases like session rotation on error.

Current Behavior

app.get("/test", async ({ cookie }) => {
    cookie.session.value = "new-session-id"
    throw new Error("unexpected error")
})

Before:

  • Response status: 500 ✓
  • Set-Cookie header: missing

This affects:

  • Session rotation (security-critical)
  • Any cookies set before errors
  • Both regular errors and validation errors
  • Both dynamic and AOT-compiled handlers

User Impact

When rotating session cookies on errors (e.g., after detecting suspicious activity), the new session ID is never sent to the client. This leaves the user's session in an invalid state, effectively logging them out unexpectedly.

Root Cause

Error responses were being created directly via new Response() without calling mapResponse(), which is responsible for serializing cookies from context.set.cookie into Set-Cookie headers via handleSet().

This happened in two places:

  1. Dynamic error handler (src/dynamic-handle.ts:728-734)

    • Fallback error response created Response directly
    • Cookies in context.set.cookie were never serialized
  2. AOT compiled error handler (src/adapter/web-standard/index.ts:153-168)

    • Both validationError and unknownError handlers created Response directly
    • Applied to both WebStandard and Bun adapters (Bun inherits from WebStandard)

Solution

Changed all error response paths to use mapResponse() instead of creating Response objects directly. This ensures handleSet() is called to serialize cookies before the response is created.

Changes

1. src/dynamic-handle.ts (lines 728-732)

// Before:
return new Response(
    typeof error.cause === 'string' ? error.cause : error.message,
    {
        headers: context.set.headers as any,
        status: error.status ?? 500
    }
)

// After:
context.set.status = error.status ?? 500
return mapResponse(
    typeof error.cause === 'string' ? error.cause : error.message,
    context.set
)

2. src/adapter/web-standard/index.ts (lines 153-158)

// Before:
validationError:
    `return new Response(` +
    `error.message,` +
    `{` +
    `headers:Object.assign({'content-type':'application/json'},set.headers),` +
    `status:set.status` +
    `}` +
    `)`,
unknownError:
    `return new Response(` +
    `error.message,` +
    `{headers:set.headers,status:error.status??set.status??500}` +
    `)`

// After:
validationError:
    `set.headers['content-type']='application/json';` +
    `return mapResponse(error.message,set)`,
unknownError:
    `set.status=error.status??set.status??500;` +
    `return mapResponse(error.message,set)`

Design Decision

Using mapResponse() has the added benefit of preserving all headers set via context.set.headers, not just cookies. This means CORS headers, custom authentication headers, and other metadata set before an error occurs are also preserved in the error response.

Testing

Added 9 comprehensive test cases covering:

  1. Set-Cookie with regular Error
  2. Set-Cookie with response validation errors
  3. Set-Cookie with onError hooks
  4. Set-Cookie with NotFoundError
  5. Set-Cookie with InternalServerError
  6. Set-Cookie in AOT mode
  7. Multiple cookies preserved
  8. Custom headers preserved alongside cookies
  9. Headers from set.headers preserved

Example

After the fix:

app.get("/test", async ({ cookie, set }) => {
    cookie.session.value = "new-session-id"
    set.headers['x-request-id'] = 'abc123'
    throw new Error("unexpected error")
})

After:

  • Response status: 500 ✓
  • Set-Cookie header: session=new-session-id; Path=/
  • X-Request-ID header: abc123

Breaking Changes

None. This fixes behavior that was clearly broken - cookies and headers set before errors should be preserved in error responses.

Summary by CodeRabbit

  • Bug Fixes
    • Error responses now consistently apply status and JSON content-type while preserving headers and cookies across error scenarios.
  • Refactor
    • Internal handling of error/response construction updated for more consistent runtime behavior and stricter type checks.
  • Tests
    • Added comprehensive tests validating header and cookie preservation, status codes, and messages for various error cases.

@coderabbitai
Copy link

coderabbitai bot commented Nov 15, 2025

Walkthrough

Delegates error response construction to mapResponse by mutating the response set (headers/status) instead of building new Response objects, and converts ElysiaErrors to a type-only import. Adds tests ensuring Set-Cookie and other headers are preserved across various error scenarios.

Changes

Cohort / File(s) Summary
Adapter: error composition
src/adapter/web-standard/index.ts
Replaced direct Response construction in composeError with mutations to set (e.g., set.headers['content-type']='application/json', set.status = error.status ?? set.status ?? 500) and delegated final response creation to mapResponse.
Runtime/handler adjustments
src/dynamic-handle.ts
Switched ElysiaErrors to a type-only import (import { type ElysiaErrors, ... }), reorganized several imports (added parseCookie, adjusted type/value imports), replaced some @ts-ignore with @ts-expect-error, and changed error paths to mutate context.set and return mapResponse(context.response, context.set) instead of constructing Responses directly.
Tests: cookie/error coverage
test/core/handle-error.test.ts
Added tests verifying Set-Cookie preservation and correct status/messages across error scenarios (generic errors, validation errors, onError hook, NotFound/InternalServer errors, AOT mode, multiple cookies, and custom headers).

Sequence Diagram(s)

sequenceDiagram
    participant H as Route Handler
    participant C as Error Catch
    participant M as mapResponse
    participant Client as Client

    rect rgba(255,120,120,0.15)
    Note over H,Client: OLD — built Response directly (problem: Set-Cookie lost)
    H->>H: set.cookie.foo = "v"
    H->>H: throw Error()
    C->>C: Construct new Response(JSON, headers, status)
    C->>Client: Response (Set-Cookie may be missing)
    end

    rect rgba(120,220,140,0.12)
    Note over H,Client: NEW — mutate set and use mapResponse (preserves cookies)
    H->>H: set.cookie.foo = "v"
    H->>H: throw Error()
    C->>C: set.headers['content-type']='application/json'
    C->>C: set.status = error.status ?? set.status ?? 500
    C->>M: mapResponse(error.message, set)
    M->>Client: Response (includes Set-Cookie & headers)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Pay attention to: adapter error path in src/adapter/web-standard/index.ts and its interaction with mapResponse.
  • Verify src/dynamic-handle.ts type-only import changes do not affect runtime exports or bundling.
  • Review new tests in test/core/handle-error.test.ts for correctness and coverage.

Possibly related PRs

Poem

🐰 I set a cookie, then trouble came,
But mapResponse kept it safe by name.
No crumbs were lost when errors hopped in,
Cookies sail onward — a rabbit’s win! 🍪✨

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR directly addresses #1547 by modifying error handling paths to use mapResponse() instead of direct Response construction, ensuring cookies and headers set via context.set are preserved in error responses.
Out of Scope Changes check ✅ Passed All changes are directly related to the objective of preserving Set-Cookie headers in error responses; no unrelated modifications were introduced.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly and concisely summarizes the main fix: Set-Cookie headers preservation during error handling, which directly matches the PR's primary objective.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 Nov 15, 2025

Open in StackBlitz

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

commit: 095bb1d

@MarcelOlsen
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Nov 15, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 78f4828 and bdf1492.

📒 Files selected for processing (3)
  • src/adapter/web-standard/index.ts (1 hunks)
  • src/dynamic-handle.ts (17 hunks)
  • test/core/handle-error.test.ts (1 hunks)
🔇 Additional comments (4)
src/dynamic-handle.ts (2)

6-6: LGTM: Type-only import correctly applied.

The change to import ElysiaErrors as a type-only import is appropriate since it's used exclusively for type annotations. This improves tree-shaking and clarifies that ElysiaErrors has no runtime presence, while preserving runtime imports for NotFoundError, status, and ValidationError.


728-732: Excellent fix: Error responses now preserve cookies and headers.

The change to use mapResponse(error message, context.set) instead of constructing a Response object directly ensures that cookies and headers set via context.set are properly serialized into error responses. This correctly addresses the root cause described in issue #1547.

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

553-658: Excellent test coverage for cookie preservation in error scenarios.

The nine new test cases provide comprehensive coverage of the fix:

  • Various error types (generic Error, ValidationError, NotFoundError, InternalServerError)
  • Different error sources (handler throws, validation failure, onError hook)
  • Both runtime and AOT modes
  • Edge cases (multiple cookies, cookies + custom headers)

All tests are well-structured with clear assertions verifying both the error status codes and the presence of set-cookie headers.

src/adapter/web-standard/index.ts (1)

154-158: LGTM: AOT error handling now preserves cookies and headers.

The changes correctly update error response handling for AOT-compiled code:

  • Validation errors: Sets content-type and delegates to mapResponse
  • Unknown errors: Sets status with proper fallback chain and delegates to mapResponse

By mutating set.headers and set.status before calling mapResponse, the AOT adapter now preserves cookies and headers in error responses, consistent with the dynamic handler fix. This ensures the fix works in both runtime and AOT modes.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@MarcelOlsen MarcelOlsen force-pushed the fix/set-cookie-in-error-handler branch from 7b088f7 to 095bb1d Compare November 15, 2025 16:01
@MarcelOlsen MarcelOlsen marked this pull request as ready for review November 15, 2025 16:02
@MarcelOlsen MarcelOlsen changed the title Fix/set cookie in error handler 🔧 Fix Set-Cookie headers not sent when errors are thrown Nov 15, 2025
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.

Set-Cookie not sent to client when Error() on handler

1 participant