diff --git a/packages/evlog/src/hono/index.ts b/packages/evlog/src/hono/index.ts index 32f38ee9..bdc9d409 100644 --- a/packages/evlog/src/hono/index.ts +++ b/packages/evlog/src/hono/index.ts @@ -63,11 +63,8 @@ export function evlog(options: EvlogHonoOptions = {}): MiddlewareHandler { try { await next() if (shouldDeferEmitForResponse(c.res)) { - const response = new Response(c.res.body, { - status: c.res.status, - headers: c.res.headers, - }) - return finishResponse(response, { status: response.status }) + c.res = await finishResponse(c.res, { status: c.res.status }) + return } await finish({ status: c.res.status }) } catch (error) { diff --git a/packages/evlog/test/frameworks/hono.test.ts b/packages/evlog/test/frameworks/hono.test.ts index 2c01d024..d0447aa1 100644 --- a/packages/evlog/test/frameworks/hono.test.ts +++ b/packages/evlog/test/frameworks/hono.test.ts @@ -176,6 +176,61 @@ describe('evlog/hono', () => { expect(logValue).toBeUndefined() }) + describe('streaming responses', () => { + it('can consume a streaming response body after middleware wraps it', async () => { + const { drain } = createPipelineSpies() + const app = new Hono() + app.use(evlog({ drain })) + + app.get('/api/stream', () => { + const encoder = new TextEncoder() + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode('chunk1')) + controller.enqueue(encoder.encode('chunk2')) + controller.close() + }, + }) + return new Response(stream, { + headers: { 'content-type': 'text/event-stream', 'x-vercel-ai-ui-message-stream': 'v1' }, + }) + }) + + const res = await app.request('/api/stream') + expect(res.status).toBe(200) + // Body must not be locked — consuming it should succeed + const text = await res.text() + expect(text).toBe('chunk1chunk2') + + await waitForDrainCalls(drain) + assertHttpEventEmitted(drain, { path: '/api/stream', status: 200 }) + }) + + it('defers drain until stream completes', async () => { + const { drain } = createPipelineSpies() + const app = new Hono() + app.use(evlog({ drain })) + + const { createDeferredStream } = await import('../helpers/stream') + const source = createDeferredStream() + + app.get('/api/defer', () => { + return new Response(source.stream, { + headers: { 'content-type': 'text/event-stream' }, + }) + }) + + const res = await app.request('/api/defer') + expect(res.status).toBe(200) + expect(drain).not.toHaveBeenCalled() + + source.close() + await res.text() + await waitForDrainCalls(drain) + assertHttpEventEmitted(drain, { path: '/api/defer', status: 200 }) + }) + }) + describe('drain / enrich / keep', () => { it('calls drain with emitted event (shared helpers)', async () => { const { drain } = createPipelineSpies()