Merge pull request #1679 from raunak-rpm/fix/async-generator-headers-1677

fix: preserve headers when throwing from AsyncGenerator
This commit is contained in:
SaltyAom
2026-02-09 10:56:25 +07:00
committed by GitHub
2 changed files with 57 additions and 2 deletions
+14 -2
View File
@@ -374,7 +374,14 @@ export const isAsync = (v: Function | HookContainer) => {
if (isObject && v.isAsync !== undefined) return v.isAsync
const fn = isObject ? v.fn : v
if (fn.constructor.name === 'AsyncFunction') return true
// Check for both AsyncFunction and AsyncGeneratorFunction
// AsyncGeneratorFunction needs to be treated as async because generator.next()
// returns a Promise that may reject (e.g., when throwing from the generator)
if (
fn.constructor.name === 'AsyncFunction' ||
fn.constructor.name === 'AsyncGeneratorFunction'
)
return true
const literal: string = fn.toString()
@@ -862,7 +869,12 @@ export const composeHandler = ({
const mapResponse = (r = 'r') => {
const after = afterResponse()
const response = `${hasSet ? 'mapResponse' : 'mapCompactResponse'}(${saveResponse}${r}${hasSet ? ',c.set' : ''}${mapResponseContext})\n`
// When maybeStream is true, mapResponse may return a Promise (from handleStream)
// that can reject if the generator throws. We need to await it so the try-catch
// can properly catch the rejection and route it to error handling.
// Only add await if the function is async (maybeAsync), otherwise it would be a syntax error.
const awaitStream = maybeStream && maybeAsync ? 'await ' : ''
const response = `${awaitStream}${hasSet ? 'mapResponse' : 'mapCompactResponse'}(${saveResponse}${r}${hasSet ? ',c.set' : ''}${mapResponseContext})\n`
if (!after) return `return ${response}`
+43
View File
@@ -561,6 +561,49 @@ describe('Stream', () => {
expect(response.headers.get('content-type')).toBe('text/event-stream')
})
// Issue #1677: Throwing from AsyncGenerator should preserve headers
it('should preserve headers when throwing from async generator', async () => {
const { status: statusFn } = await import('../../src')
const app = new Elysia().get('/', async function* ({ set }) {
set.headers['access-control-allow-origin'] = '*'
set.headers['x-custom-header'] = 'test-value'
// Throw before yielding - this is the bug scenario from #1677
if (true) throw statusFn(500)
yield 'unreachable'
})
const response = await app.handle(req('/'))
expect(response.status).toBe(500)
expect(response.headers.get('access-control-allow-origin')).toBe('*')
expect(response.headers.get('x-custom-header')).toBe('test-value')
})
// Issue #1677: onError hook should be called when throwing from generator
it('should call onError hook when throwing from async generator', async () => {
const { status: statusFn } = await import('../../src')
let onErrorCalled = false
let errorCode: string | number | undefined
const app = new Elysia()
.onError(({ code }) => {
onErrorCalled = true
errorCode = code
})
.get('/', async function* ({ set }) {
set.headers['x-custom-header'] = 'test-value'
// Throw before yielding - this is the bug scenario from #1677
if (true) throw statusFn(500)
yield 'unreachable'
})
const response = await app.handle(req('/'))
expect(response.status).toBe(500)
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' })