Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/adapter/bun/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,5 +567,6 @@ const handleResponse = createResponseHandler({

const handleStream = createStreamHandler({
mapResponse,
mapCompactResponse
mapCompactResponse,
streamOptions: {}
})
Comment on lines 568 to 572
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify that ElysiaConfig.stream is propagated to this handler.

The streamOptions is hardcoded to an empty object {}, which means the autoCancellation setting will always fall back to the environment variable default. There's no visible mechanism to pass the ElysiaConfig.stream configuration (defined in src/types.ts lines 171-184) to this handler.

Run the following script to check if there's a code path that passes the config from ElysiaConfig to the handlers:


🏁 Script executed:

#!/bin/bash
# Search for where ElysiaConfig.stream is accessed and passed to handlers
rg -n -C3 "config\.stream|\.stream\s*\?" --type=ts -g '!test/**' -g '!node_modules/**'

Length of output: 0


I need to investigate this more thoroughly with targeted searches. The initial pattern returned no results, so let me try different approaches to understand how the configuration flows.

Let me search the actual codebase to examine the ElysiaConfig definition and handler implementation.





---

🏁 Script executed:

```shell
#!/bin/bash
# Search for ElysiaConfig type definition and stream property
rg -n "type ElysiaConfig|interface ElysiaConfig" --type=ts -A 30 -g '!test/**' -g '!node_modules/**'

Length of output: 1130


🏁 Script executed:

#!/bin/bash
# Get the content around the handler initialization
cat src/adapter/bun/handler.ts | sed -n '560,580p'

Length of output: 231


🏁 Script executed:

#!/bin/bash
# Search for createStreamHandler definition
rg -n "createStreamHandler|export.*createStreamHandler" --type=ts -B 5 -A 10 -g '!test/**' -g '!node_modules/**'

Length of output: 4831


I need to see the full ElysiaConfig definition and find where the stream property is defined, plus understand how the handlers are invoked with configuration.





---

🏁 Script executed:

```shell
#!/bin/bash
# Get more of the ElysiaConfig definition, especially around lines 171-184
cat src/types.ts | sed -n '79,200p'

Length of output: 2774


🏁 Script executed:

#!/bin/bash
# Search for where handleStream is actually called/used
rg -n "handleStream\(" --type=ts -B 3 -A 3 -g '!test/**' -g '!node_modules/**'

Length of output: 5489


🏁 Script executed:

#!/bin/bash
# Search for stream property in ElysiaConfig or related config
rg -n "stream\s*\?:|stream:" --type=ts -B 2 -A 2 -g '!test/**' -g '!node_modules/**' | head -40

Length of output: 517


I need to find where these handlers are exported and how the Elysia class uses them with the configuration.





---

🏁 Script executed:

```shell
#!/bin/bash
# Search for where handleStream/handleResponse are exported from handlers
rg -n "export.*handleStream|export.*handleResponse" --type=ts -g '!test/**' -g '!node_modules/**'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find the Elysia class that would use these handlers
rg -n "class Elysia|export.*class Elysia" --type=ts -A 20 -g '!test/**' -g '!node_modules/**' | head -50

Length of output: 2891


🏁 Script executed:

#!/bin/bash
# Search for where createHandler is called to see if config is passed
rg -n "createStreamHandler\(" --type=ts -B 5 -A 5 -g '!test/**' -g '!node_modules/**'

Length of output: 1644


Let me search for the main Elysia class and how it uses these handler modules:





---

🏁 Script executed:

```shell
#!/bin/bash
# Find main Elysia class definition (not ElysiaRequest, etc)
find src -name "*.ts" ! -path "*/test/*" ! -path "*/node_modules/*" -exec grep -l "^export class Elysia" {} \;

Length of output: 75


🏁 Script executed:

#!/bin/bash
# Search for imports of the bun handler module
rg -n "from.*adapter/bun/handler|import.*handler.*from" --type=ts -B 2 -A 2 -g '!test/**' -g '!node_modules/**' | head -50

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for where adapter handlers are instantiated with config
rg -n "new.*Adapter|adapter\(" --type=ts -B 3 -A 3 -g '!test/**' -g '!node_modules/**' | head -60

Length of output: 0


Let me search the repository structure more broadly:





---

🏁 Script executed:

```shell
#!/bin/bash
# List src directory structure
ls -la src/

Length of output: 1222


🏁 Script executed:

#!/bin/bash
# Search for all class definitions more broadly
rg -n "^export (default )?class" --type=ts -g '!test/**' -g '!node_modules/**'

Length of output: 847


🏁 Script executed:

#!/bin/bash
# Look for index files that might be entry points
cat src/index.ts 2>/dev/null || echo "No src/index.ts found"

Length of output: 187196


ElysiaConfig.stream is not propagated to handler instantiation.

ElysiaConfig defines a stream property (src/types.ts:174) with autoCancellation?: boolean, but the stream handlers are instantiated at module load time with hardcoded empty options. At src/adapter/bun/handler.ts:568-572 and src/adapter/web-standard/handler.ts:601-605, createStreamHandler is called with streamOptions: {}. The createStreamHandler function falls back to environment variable defaults when streamOptions is empty, meaning instance-level config is ignored entirely.

To fix this, streamOptions must be populated from the Elysia instance's config at handler composition time, not at module load time. This likely requires refactoring the handler instantiation to be request-aware or instance-aware rather than module-level singletons.

🤖 Prompt for AI Agents
In src/adapter/bun/handler.ts around lines 568-572, the stream handler is
created at module load with streamOptions: {} so per-instance
ElysiaConfig.stream (autoCancellation etc.) is never applied; change the code to
instantiate the stream handler at handler-composition time using the Elysia
instance/config (or accept streamOptions as an argument) and pass
ElysiaConfig.stream into createStreamHandler instead of {} so instance-level
stream settings are respected; apply the same refactor to
src/adapter/web-standard/handler.ts where createStreamHandler is currently
called with an empty options object.

21 changes: 14 additions & 7 deletions src/adapter/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,17 +143,22 @@ type CreateHandlerParameter = {
request?: Request
): Response
mapCompactResponse(response: unknown, request?: Request): Response
streamOptions?: {
autoCancellation?: boolean
}
}

const allowRapidStream = env.ELYSIA_RAPID_STREAM === 'true'
const defaultAutoCancellation = env.ELYSIA_STREAM_AUTO_CANCELLATION !== 'false'

export const createStreamHandler =
({ mapResponse, mapCompactResponse }: CreateHandlerParameter) =>
({ mapResponse, mapCompactResponse, streamOptions }: CreateHandlerParameter) =>
async (
generator: Generator | AsyncGenerator | ReadableStream,
set?: Context['set'],
request?: Request
) => {
const autoCancellation = streamOptions?.autoCancellation ?? defaultAutoCancellation
// Since ReadableStream doesn't have next, init might be undefined
let init = (generator as Generator).next?.() as
| IteratorResult<unknown>
Expand Down Expand Up @@ -214,13 +219,15 @@ export const createStreamHandler =
async start(controller) {
let end = false

request?.signal?.addEventListener('abort', () => {
end = true
if (autoCancellation) {
request?.signal?.addEventListener('abort', () => {
end = true

try {
controller.close()
} catch {}
})
try {
controller.close()
} catch {}
})
}

if (!init || init.value instanceof ReadableStream) {
} else if (
Expand Down
3 changes: 2 additions & 1 deletion src/adapter/web-standard/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -600,5 +600,6 @@ const handleResponse = createResponseHandler({

const handleStream = createStreamHandler({
mapResponse,
mapCompactResponse
mapCompactResponse,
streamOptions: {}
})
Comment on lines 601 to 605
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify that ElysiaConfig.stream is propagated to this handler.

The streamOptions is hardcoded to an empty object {}, which means the autoCancellation setting will always fall back to the environment variable default. There's no visible mechanism to pass the ElysiaConfig.stream configuration (defined in src/types.ts lines 171-184) to this handler.

Run the following script to check if there's a code path that passes the config from ElysiaConfig to the handlers:


🏁 Script executed:

#!/bin/bash
# Search for where ElysiaConfig.stream is accessed and passed to handlers
rg -n -C3 "config\.stream|\.stream\s*\?" --type=ts -g '!test/**' -g '!node_modules/**'

Length of output: 0


I'll conduct a more comprehensive search to trace how configuration flows through the codebase. The initial pattern search produced no results, so I need a broader investigation.

I need to examine the actual codebase to understand the configuration flow. Let me search more broadly in the handler file and find where the configuration is passed.


---

🏁 Script executed:

```shell
#!/bin/bash
# View the handler.ts file context around lines 601-605
sed -n '590,620p' src/adapter/web-standard/handler.ts

Length of output: 296


🏁 Script executed:

#!/bin/bash
# Search for createStreamHandler definition and calls
rg -n "createStreamHandler" --type=ts -g '!node_modules/**' -B2 -A5

Length of output: 4601


🏁 Script executed:

#!/bin/bash
# Find ElysiaConfig type definition
rg -n "interface ElysiaConfig|type ElysiaConfig" --type=ts -g '!node_modules/**' -A20

Length of output: 802


🏁 Script executed:

#!/bin/bash
# Search for how stream config is used across the codebase
rg -n "stream\s*:" --type=ts -g '!node_modules/**' -C2 | head -50

Length of output: 3617


🏁 Script executed:

#!/bin/bash
# Find where the handler function is called/created with config
rg -n "createStreamHandler\s*\(" --type=ts -g '!node_modules/**' -B5 -A10

Length of output: 3789


I need to verify the complete ElysiaConfig definition to see if a stream property exists.


---

🏁 Script executed:

```shell
#!/bin/bash
# View the full ElysiaConfig interface, especially around lines 171-184
sed -n '79,200p' src/types.ts

Length of output: 2774


🏁 Script executed:

#!/bin/bash
# Search for stream property in ElysiaConfig
rg -n "stream" src/types.ts -C3

Length of output: 2050


🏁 Script executed:

#!/bin/bash
# Check how handlers are instantiated and if config is passed at runtime
rg -n "handleStream|handleResponse" src/adapter/web-standard/handler.ts -C5

Length of output: 3649


🏁 Script executed:

#!/bin/bash
# Look for where ElysiaConfig is used in handler initialization
rg -n "ElysiaConfig" src/adapter/web-standard/handler.ts -C3

Length of output: 0


Pass ElysiaConfig.stream configuration to createStreamHandler in both adapters.

ElysiaConfig.stream defines an autoCancellation property (lines 174-182 of src/types.ts), but the handlers in both adapters hardcode streamOptions: {} at module initialization, ignoring user configuration. This forces autoCancellation to fall back to the environment variable default instead of respecting the per-instance config.

Both src/adapter/web-standard/handler.ts (lines 601-605) and src/adapter/bun/handler.ts (lines 568-572) need to pass the ElysiaConfig.stream value to createStreamHandler. Since handlers are module-level constants, you'll need to refactor them to be created dynamically per Elysia instance or injected with config at runtime.

14 changes: 14 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,20 @@ export interface ElysiaConfig<Prefix extends string | undefined> {
WebSocketHandler<any>,
'open' | 'close' | 'message' | 'drain'
>
/**
* Stream response configuration
*/
stream?: {
/**
* Enable automatic cancellation of streams when the client disconnects
*
* When enabled, Elysia will automatically stop generator functions
* if the client cancels the request before streaming is completed
*
* @default true
*/
autoCancellation?: boolean
}
cookie?: CookieOptions & {
/**
* Specified cookie name to be signed globally
Expand Down
118 changes: 118 additions & 0 deletions test/response/stream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,4 +560,122 @@ describe('Stream', () => {
expect(result).toEqual(['Elysia', 'Eden'].map((x) => `data: ${x}\n\n`))
expect(response.headers.get('content-type')).toBe('text/event-stream')
})


it('continue stream when autoCancellation is disabled', async () => {
const { createStreamHandler } = await import('../../src/adapter/utils')
const sideEffects: string[] = []

const mockMapResponse = (value: any) =>
new Response(value)
const mockMapCompactResponse = (value: any) =>
new Response(value)

const streamHandler = createStreamHandler({
mapResponse: mockMapResponse,
mapCompactResponse: mockMapCompactResponse,
streamOptions: { autoCancellation: false }
})

async function* testGenerator() {
sideEffects.push('a')
yield 'a'
await Bun.sleep(20)

sideEffects.push('b')
yield 'b'
await Bun.sleep(20)

sideEffects.push('c')
yield 'c'
}

const controller = new AbortController()
const request = new Request('http://e.ly', {
signal: controller.signal
})

setTimeout(() => {
controller.abort()
}, 15)

const response = await streamHandler(testGenerator(), undefined, request)
const reader = response.body?.getReader()

if (!reader) throw new Error('No reader')

const results: string[] = []
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
results.push(new TextDecoder().decode(value))
}
} catch (e) {
// Expected to error when abort happens
}

await Bun.sleep(100)

expect(sideEffects).toEqual(['a', 'b', 'c'])
})

it('stop stream when autoCancellation is enabled (default)', async () => {
const { createStreamHandler } = await import('../../src/adapter/utils')
const sideEffects: string[] = []

const mockMapResponse = (value: any) =>
new Response(value)
const mockMapCompactResponse = (value: any) =>
new Response(value)

const streamHandler = createStreamHandler({
mapResponse: mockMapResponse,
mapCompactResponse: mockMapCompactResponse,
streamOptions: { autoCancellation: true }
})

async function* testGenerator() {
sideEffects.push('a')
yield 'a'
await Bun.sleep(20)

sideEffects.push('b')
yield 'b'
await Bun.sleep(20)

sideEffects.push('c')
yield 'c'
}

const controller = new AbortController()
const request = new Request('http://e.ly', {
signal: controller.signal
})

setTimeout(() => {
controller.abort()
}, 15)

const response = await streamHandler(testGenerator(), undefined, request)
const reader = response.body?.getReader()

if (!reader) throw new Error('No reader')

const results: string[] = []
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
results.push(new TextDecoder().decode(value))
}
} catch (e) {
// Expected to error when abort happens
}

await Bun.sleep(100)

expect(sideEffects).toHaveLength(2)
expect(sideEffects).toEqual(['a', 'b'])
})
})