From c29f61e88e75592d8bea71667e41c0bd79cf1ce4 Mon Sep 17 00:00:00 2001 From: RAUNAK <122172696+aunak@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:05:20 +0530 Subject: [PATCH 1/4] Fix SSE double-wrapping bug when returning pre-formatted Response with chunked transfer Fixes #1661 Problem: When returning a Response with pre-formatted SSE data (content-type: text/event-stream and transfer-encoding: chunked) while using set.headers, the response would be double- wrapped with 'data:' prefix, resulting in 'data: data: hello' instead of 'data: hello'. Root Cause: - handleResponse merges set.headers into the Response - Sees transfer-encoding: chunked and routes through handleStream(streamResponse(response)) - handleStream detects SSE via content-type and auto-wraps chunks with 'data: ' format - But the Response body is ALREADY formatted as SSE by the user - Result: double-wrapping Solution: Added skipFormat parameter to createStreamHandler to prevent SSE auto-formatting when streaming pre-formatted Response bodies. The skipFormat flag is set to true when: - Response has transfer-encoding: chunked - Stream comes from streamResponse(response) (already formatted by user) This ensures: - Generator functions still get SSE formatting applied - Pre-formatted Response bodies are streamed as-is - No behavioral changes for existing working code Changes: - src/adapter/utils.ts: Added skipFormat parameter to createStreamHandler - src/adapter/utils.ts: Pass skipFormat=true for Response body streams - test/response/sse-double-wrap.test.ts: Added comprehensive regression tests Testing: - All 1434 existing tests pass - 3 new tests cover the bug scenarios - Verified generator functions still work correctly --- src/adapter/utils.ts | 25 +++++++---- test/response/sse-double-wrap.test.ts | 63 +++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 test/response/sse-double-wrap.test.ts diff --git a/src/adapter/utils.ts b/src/adapter/utils.ts index 0d889e6c..94a2ab22 100644 --- a/src/adapter/utils.ts +++ b/src/adapter/utils.ts @@ -152,7 +152,8 @@ export const createStreamHandler = async ( generator: Generator | AsyncGenerator | ReadableStream, set?: Context['set'], - request?: Request + request?: Request, + skipFormat?: boolean ) => { // Since ReadableStream doesn't have next, init might be undefined let init = (generator as Generator).next?.() as @@ -171,13 +172,17 @@ export const createStreamHandler = return mapCompactResponse(init.value, request) } + // Check if stream is from a pre-formatted Response body const isSSE = - // @ts-ignore First SSE result is wrapped with sse() - init?.value?.sse ?? - // @ts-ignore ReadableStream is wrapped with sse() - generator?.sse ?? - // User explicitly set content-type to SSE - set?.headers['content-type']?.startsWith('text/event-stream') + !skipFormat && + ( + // @ts-ignore First SSE result is wrapped with sse() + init?.value?.sse ?? + // @ts-ignore ReadableStream is wrapped with sse() + generator?.sse ?? + // User explicitly set content-type to SSE + set?.headers['content-type']?.startsWith('text/event-stream') + ) const format = isSSE ? (data: string) => `data: ${data}\n\n` @@ -385,7 +390,8 @@ export const createResponseHandler = (handler: CreateHandlerParameter) => { return handleStream( streamResponse(newResponse as Response), responseToSetHeaders(newResponse as Response, set), - request + request, + true // skipFormat: don't auto-format SSE for pre-formatted Response ) as any return newResponse @@ -399,7 +405,8 @@ export const createResponseHandler = (handler: CreateHandlerParameter) => { return handleStream( streamResponse(response as Response), responseToSetHeaders(response as Response, set), - request + request, + true // skipFormat: don't auto-format SSE for pre-formatted Response ) as any return response diff --git a/test/response/sse-double-wrap.test.ts b/test/response/sse-double-wrap.test.ts new file mode 100644 index 00000000..28a97666 --- /dev/null +++ b/test/response/sse-double-wrap.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect } from 'bun:test' +import { Elysia } from '../../src' + +describe('SSE - Response Double Wrapping', () => { + it('should not double-wrap SSE data when returning pre-formatted Response', async () => { + const app = new Elysia().get('/', ({ set }) => { + set.headers.hello = 'world' + + return new Response('data: hello\n\ndata: world\n\n', { + headers: { + 'content-type': 'text/event-stream', + 'transfer-encoding': 'chunked' + }, + status: 200 + }) + }) + + const response = await app.handle(new Request('http://localhost/')).then(r => r.text()) + + // Should NOT double-wrap with "data: data:" + expect(response).toBe('data: hello\n\ndata: world\n\n') + expect(response).not.toContain('data: data:') + }) + + it('should not double-wrap SSE when using set.headers with pre-formatted content', async () => { + const app = new Elysia().get('/', ({ set }) => { + set.headers['x-custom'] = 'test' + set.headers['content-type'] = 'text/event-stream' + + return new Response('data: message1\n\ndata: message2\n\n', { + headers: { + 'transfer-encoding': 'chunked' + } + }) + }) + + const response = await app.handle(new Request('http://localhost/')).then(r => r.text()) + + expect(response).toBe('data: message1\n\ndata: message2\n\n') + expect(response).not.toContain('data: data:') + }) + + it('should properly format SSE for generator functions', async () => { + const app = new Elysia().get('/', function* () { + yield 'hello' + yield 'world' + }) + + const response = await app + .handle( + new Request('http://localhost/', { + headers: { + 'content-type': 'text/event-stream' + } + }) + ) + .then((r) => r.text()) + + // Generator without explicit SSE should format as plain text + expect(response).toContain('hello') + expect(response).toContain('world') + }) +}) From ec1dfb57726ff0fcf26dfb0e9eaf3452a19195cd Mon Sep 17 00:00:00 2001 From: RAUNAK <122172696+aunak@users.noreply.github.com> Date: Wed, 7 Jan 2026 20:14:20 +0530 Subject: [PATCH 2/4] Add test for generators with explicit SSE configuration - Tests that generators using sse() helper get proper SSE formatting - Verifies no double-wrapping occurs with explicit SSE markers - Addresses CodeRabbit review suggestion for comprehensive SSE test coverage --- test/response/sse-double-wrap.test.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/response/sse-double-wrap.test.ts b/test/response/sse-double-wrap.test.ts index 28a97666..4c507a7f 100644 --- a/test/response/sse-double-wrap.test.ts +++ b/test/response/sse-double-wrap.test.ts @@ -60,4 +60,27 @@ describe('SSE - Response Double Wrapping', () => { expect(response).toContain('hello') expect(response).toContain('world') }) + + it('should format SSE correctly for generators with explicit SSE configuration', async () => { + const { sse } = await import('../../src') + + const app = new Elysia().get('/', ({ set }) => { + set.headers['content-type'] = 'text/event-stream' + + return (async function* () { + yield sse({ data: 'first message' }) + yield sse({ data: 'second message' }) + })() + }) + + const response = await app + .handle(new Request('http://localhost/')) + .then((r) => r.text()) + + // Generator WITH explicit SSE markers should get properly formatted + expect(response).toContain('data: first message\n\n') + expect(response).toContain('data: second message\n\n') + // Should NOT double-wrap + expect(response).not.toContain('data: data:') + }) }) From 426f98c4c09a278ec6300b4cf81d2784a4631fc8 Mon Sep 17 00:00:00 2001 From: RAUNAK <122172696+aunak@users.noreply.github.com> Date: Wed, 7 Jan 2026 20:24:27 +0530 Subject: [PATCH 3/4] Fix: Remove misleading request header from generator test - Removed content-type header from GET request (doesn't affect response) - Request Content-Type is irrelevant for GET responses - Addresses CodeRabbit review feedback --- test/response/sse-double-wrap.test.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/test/response/sse-double-wrap.test.ts b/test/response/sse-double-wrap.test.ts index 4c507a7f..a8e9a323 100644 --- a/test/response/sse-double-wrap.test.ts +++ b/test/response/sse-double-wrap.test.ts @@ -47,13 +47,7 @@ describe('SSE - Response Double Wrapping', () => { }) const response = await app - .handle( - new Request('http://localhost/', { - headers: { - 'content-type': 'text/event-stream' - } - }) - ) + .handle(new Request('http://localhost/')) .then((r) => r.text()) // Generator without explicit SSE should format as plain text From b2ad171790bdbd33d42b3a1dd4f4f0c45fddb7e5 Mon Sep 17 00:00:00 2001 From: RAUNAK <122172696+aunak@users.noreply.github.com> Date: Wed, 7 Jan 2026 22:12:23 +0530 Subject: [PATCH 4/4] Strengthen assertions: Verify plain text generators are NOT SSE formatted - Added negative assertions to confirm no 'data:' prefix - Ensures generator without sse() helper outputs plain text - Test now explicitly verifies what comment claims - Addresses CodeRabbit review feedback --- test/response/sse-double-wrap.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/response/sse-double-wrap.test.ts b/test/response/sse-double-wrap.test.ts index a8e9a323..f141fd1d 100644 --- a/test/response/sse-double-wrap.test.ts +++ b/test/response/sse-double-wrap.test.ts @@ -53,6 +53,9 @@ describe('SSE - Response Double Wrapping', () => { // Generator without explicit SSE should format as plain text expect(response).toContain('hello') expect(response).toContain('world') + // Verify it's NOT SSE formatted + expect(response).not.toContain('data: hello') + expect(response).not.toContain('data: world') }) it('should format SSE correctly for generators with explicit SSE configuration', async () => {