Skip to content

Conversation

@raunak-rpm
Copy link

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

Summary

Fixes #1677

When throwing from an AsyncGenerator handler, headers set via set.headers were not being included in the error response. This broke the CORS plugin and any other use case relying on custom headers in error responses from streaming handlers.

Root Cause

The issue had two parts:

  1. isAsync() didn't recognize AsyncGeneratorFunction: The function only checked for AsyncFunction, causing async generator handlers to be treated as synchronous. This meant the generated handler function was not async.

  2. mapResponse Promise not awaited: When handling generators, mapResponse calls handleStream which is async and returns a Promise. This Promise was returned directly without await, so when the generator threw, the Promise rejection escaped the try-catch block instead of being caught and routed through error handling.

Fix

  1. Updated isAsync() to also check for fn.constructor.name === 'AsyncGeneratorFunction'

  2. In the generated handler code, when maybeStream && maybeAsync is true, the mapResponse call is now awaited:

    // Before
    return mapResponse(r, c.set, c.request)
    
    // After  
    return await mapResponse(r, c.set, c.request)

Test Cases Added

  • should preserve headers when throwing from async generator
  • should call onError hook when throwing from async generator

Verification

Before fix:

.get('/fail', async function* ({ set }) {
    set.headers['access-control-allow-origin'] = '*'
    throw status(500)  // Headers NOT sent, error escaped app.handle()
})

After fix:

  • Headers are properly included in error response
  • onError hook is called
  • Response is properly returned (not thrown)

All 1448 tests pass.

Summary by CodeRabbit

  • Bug Fixes

    • Improved error handling in streaming so errors propagate correctly through existing try/catch paths, ensuring rejection behavior is preserved.
    • Ensured response headers are retained when errors occur during streaming and error callbacks are triggered.
  • Tests

    • Added tests verifying header preservation and that error hooks are invoked when streaming generators throw.

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

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 15, 2026

Open in StackBlitz

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

commit: 1802497

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 15, 2026

Walkthrough

Extended async detection to include AsyncGeneratorFunction and conditionally await mapResponse during streaming to ensure rejections propagate through existing error handling paths.

Changes

Cohort / File(s) Summary
Async generator & streaming logic
src/compose.ts
Treat AsyncGeneratorFunction as async; when maybeStream and maybeAsync are true, prefix mapResponse invocation with await so stream rejections flow through try/catch and existing error paths.
Stream error handling tests
test/response/stream.test.ts
Added tests verifying headers are preserved when an async generator throws and that the onError hook is called with proper status/headers in that scenario.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Handler
  participant compose.ts
  participant mapResponse
  participant handleStream
  participant onErrorHook

  Client->>Handler: send request
  Handler->>compose.ts: produce response (maybe async generator)
  compose.ts->>mapResponse: build response mapping (maybeAsync + maybeStream)
  alt streaming + async
    compose.ts->>mapResponse: await mapResponse(...) 
    mapResponse->>handleStream: return stream proxy
    handleStream->>compose.ts: error thrown in stream
    compose.ts->>onErrorHook: invoke onError with error + headers
    onErrorHook->>Client: respond with error status & headers
  else non-stream or sync
    compose.ts->>mapResponse: mapResponse(...) (no await)
    mapResponse->>Client: send response
  end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐇 I nibble through code with delight,
Awaiting streams in the soft moonlight,
Async generators hop and sing,
Errors caught on a careful wing,
Headers kept safe — a small, brave bite.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main fix: preserving headers when exceptions are thrown from AsyncGenerator handlers, which directly matches the primary objective of the changeset.
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 nitpick comments
test/response/stream.test.ts (1)

565-581: LGTM! Test correctly verifies header preservation.

The test properly exercises the issue #1677 scenario where headers set before throwing from an async generator were lost. The if (true) pattern is intentional to force the error path while maintaining generator syntax.

Minor: Consider using a static import for status alongside the existing imports from '../../src' for consistency.

Optional: Use static import
-import { Elysia, sse } from '../../src'
+import { Elysia, sse, status } from '../../src'

Then replace the dynamic imports with direct usage:

-const { status: statusFn } = await import('../../src')
-// ...
-if (true) throw statusFn(500)
+if (true) throw status(500)

📜 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 42aa87e and 1802497.

📒 Files selected for processing (2)
  • src/compose.ts
  • test/response/stream.test.ts
🧰 Additional context used
🪛 Biome (2.1.2)
test/response/stream.test.ts

[error] 572-572: Unexpected constant condition.

(lint/correctness/noConstantCondition)


[error] 597-597: Unexpected constant condition.

(lint/correctness/noConstantCondition)

🔇 Additional comments (3)
src/compose.ts (2)

377-384: LGTM! Correct fix for async generator detection.

The AsyncGeneratorFunction constructor check is necessary because generator.next() returns a Promise that can reject. Without this, async generators were incorrectly treated as synchronous, causing the generated handler to lack the async keyword and preventing proper rejection handling.


872-877: LGTM! Proper await for streaming error propagation.

The conditional await ensures that when handleStream returns a Promise (which can reject if the generator throws), the rejection is caught by the try-catch block and routed through error handling. The maybeAsync guard prevents syntax errors in synchronous contexts.

test/response/stream.test.ts (1)

584-607: LGTM! Test correctly verifies onError hook invocation.

The test properly validates that the onError hook is called when an async generator throws, and that the error code and custom headers are preserved. This covers the second part of issue #1677's expected behavior.

✏️ 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.

@raunak-rpm raunak-rpm force-pushed the fix/async-generator-headers-1677 branch from c74e6cb to 42aa87e Compare January 15, 2026 18:35
When throwing from an AsyncGenerator handler, headers set via set.headers
were not being included in the error response. This was due to two issues:

1. AsyncGeneratorFunction was not being detected as async by isAsync(),
   causing the generated handler to be synchronous.

2. mapResponse returns a Promise when handling generators (via handleStream),
   but this Promise was not being awaited. When the generator threw, the
   Promise rejection escaped the try-catch block.

The fix:
- Updated isAsync() to recognize AsyncGeneratorFunction as async
- Added 'await' to mapResponse when maybeStream && maybeAsync is true,
  ensuring Promise rejections from handleStream are properly caught
  and routed through error handling with preserved headers

This ensures CORS headers and other custom headers work correctly when
using throw inside async generator handlers.

Closes elysiajs#1677
@raunak-rpm raunak-rpm force-pushed the fix/async-generator-headers-1677 branch from 42aa87e to 1802497 Compare January 15, 2026 18:42
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.

Throwing from AsyncGenerator does not send headers

2 participants