Skip to content

Conversation

@Aunali321
Copy link

@Aunali321 Aunali321 commented Nov 1, 2025

This PR adds a configuration option to control automatic cancellation of generator-based streams when clients disconnect.

Changes:

  • Added stream config option to ElysiaConfig with autoCancellation flag
  • Updated CreateHandlerParameter to accept streamOptions
  • Updated createStreamHandler to respect autoCancellation setting
  • Added ELYSIA_STREAM_AUTO_CANCELLATION environment variable support
  • Updated both Bun and Web Standard handlers to use the new option
  • Added comprehensive tests for both enabled and disabled autoCancellation behavior

By default, autoCancellation is enabled (true) to maintain backward compatibility.

Usage:

  1. Environment variable: ELYSIA_STREAM_AUTO_CANCELLATION=false
  2. Programmatic: pass streamOptions to createStreamHandler

Addresses the use case where developers need generator functions to continue executing even after a client disconnects.

Motivation:

Elysia's automatic stream cancellation on client disconnect prevents valid use cases where server operations should complete regardless of connection status (e.g., AI agent executions that persist results to database, paid API calls that cannot be resumed, background job processing).

This PR adds an opt-in configuration flag to disable auto-cancellation while maintaining backward compatibility, enabling developers to build resilient systems where long-running operations complete even when clients disconnect and reconnect later.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a new stream configuration option enabling automatic cancellation of streaming responses when clients disconnect. This feature is enabled by default, ensuring graceful termination of ongoing streaming operations upon client disconnection.
  • Tests

    • Added comprehensive tests validating auto-cancellation behavior for streaming responses with the feature both enabled and disabled.

@coderabbitai
Copy link

coderabbitai bot commented Nov 1, 2025

Walkthrough

This change introduces a new streamOptions configuration parameter with an autoCancellation flag that controls whether streaming generators are automatically terminated when clients disconnect. The implementation adds the configuration across adapters, updates type definitions, and includes test coverage for both enabled and disabled states.

Changes

Cohort / File(s) Summary
Configuration type definitions
src/types.ts
Adds optional stream?: { autoCancellation?: boolean } to ElysiaConfig to expose autoCancellation configuration at the top level
Adapter utilities
src/adapter/utils.ts
Adds streamOptions?: { autoCancellation?: boolean } to CreateHandlerParameter interface; implements autoCancellation logic to compute the flag value and conditionally attach abort handler that closes stream controller on request abort
Adapter handlers
src/adapter/bun/handler.ts, src/adapter/web-standard/handler.ts
Both files pass streamOptions: {} to createStreamHandler when creating handleStream
Stream tests
test/response/stream.test.ts
Adds two test cases validating autoCancellation disabled (streams all yielded values despite abort) and enabled (stops streaming on abort) scenarios

Sequence Diagram

sequenceDiagram
    participant Client
    participant Handler
    participant StreamController
    participant Generator

    Client->>Handler: Request stream
    Handler->>StreamController: Create stream
    alt autoCancellation: true
        Client->>Handler: Disconnect/Abort
        Handler->>StreamController: Attach abort listener
        StreamController-->>Handler: Abort event fired
        Handler->>StreamController: controller.close()
        StreamController->>Generator: Stop iteration
    else autoCancellation: false
        Client->>Handler: Disconnect/Abort
        Handler->>StreamController: No abort listener attached
        Generator->>StreamController: Continue yielding values
        StreamController->>Client: Send remaining chunks
    end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

  • The changes follow a consistent pattern: adding an optional streamOptions configuration field across all adapters
  • The core logic is straightforward conditional attachment of an abort handler
  • Test coverage validates both enabled and disabled states
  • Minor concern: verify that the abort handler correctly closes the stream controller without resource leaks

Poem

🐰 A stream configuration brings such delight,
With autoCancellation set just right,
Abort the connection, the generator shall cease,
Or flow without stopping—choose your peace! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The PR title "feat: Add stream autoCancellation configuration option for streams" accurately reflects the main change across all modified files in the changeset. The changes collectively introduce a new optional autoCancellation configuration option that allows developers to control whether generator-based streams automatically terminate when clients disconnect. The title is concise, uses proper conventional commit format, and is specific enough that a teammate scanning the repository history would clearly understand the primary feature being added. The title directly corresponds to the PR objectives which state the feature "introduces a configuration option to control automatic cancellation of generator-based streams when clients disconnect."
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
test/response/stream.test.ts (2)

565-621: Good test coverage for autoCancellation disabled behavior.

The test correctly verifies that when autoCancellation: false, the generator continues executing after client disconnect (all three side effects occur).

Consider adding an integration test that uses the actual Elysia config option:

it('use autoCancellation from Elysia config', async () => {
  const sideEffects: string[] = []
  
  const app = new Elysia({
    stream: { autoCancellation: false }
  }).get('/', async function* () {
    sideEffects.push('a')
    yield 'a'
    await Bun.sleep(20)
    
    sideEffects.push('b')
    yield 'b'
    await Bun.sleep(20)
    
    sideEffects.push('c')
    yield 'c'
  })
  
  // ... rest of test
})

565-680: Consider reducing test duplication.

Both tests share nearly identical structure. Consider extracting a helper function to reduce duplication:

async function testStreamCancellation(
  autoCancellation: boolean,
  expectedSideEffects: string[]
) {
  const sideEffects: string[] = []
  
  const streamHandler = createStreamHandler({
    mapResponse: (value: any) => new Response(value),
    mapCompactResponse: (value: any) => new Response(value),
    streamOptions: { autoCancellation }
  })
  
  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'
  }
  
  // ... rest of test logic
  
  expect(sideEffects).toEqual(expectedSideEffects)
}

it('continue stream when autoCancellation is disabled', async () => {
  await testStreamCancellation(false, ['a', 'b', 'c'])
})

it('stop stream when autoCancellation is enabled', async () => {
  await testStreamCancellation(true, ['a', 'b'])
})
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 94abb3c and 0fa0d03.

📒 Files selected for processing (5)
  • src/adapter/bun/handler.ts (1 hunks)
  • src/adapter/utils.ts (2 hunks)
  • src/adapter/web-standard/handler.ts (1 hunks)
  • src/types.ts (1 hunks)
  • test/response/stream.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
test/response/stream.test.ts (1)
src/adapter/utils.ts (1)
  • createStreamHandler (154-302)
🔇 Additional comments (4)
src/types.ts (1)

171-184: LGTM! Clear and well-documented configuration option.

The new stream configuration block is well-structured with clear JSDoc documentation explaining the behavior and default value. The default of true maintains backward compatibility while allowing opt-out when needed.

src/adapter/utils.ts (3)

146-148: LGTM! Clear interface extension.

The streamOptions parameter is properly typed as optional with a well-defined shape.


152-152: LGTM! Appropriate default behavior.

The default from the environment variable uses !== 'false', meaning it defaults to true unless explicitly set to 'false'. This maintains backward compatibility (auto-cancellation on by default) while allowing opt-out via environment variable.


222-230: LGTM! Clean conditional abort handling.

The abort listener is only attached when autoCancellation is true, which correctly implements the feature. The try-catch around controller.close() handles potential errors gracefully.

Comment on lines 568 to 572
const handleStream = createStreamHandler({
mapResponse,
mapCompactResponse
mapCompactResponse,
streamOptions: {}
})
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.

Comment on lines 601 to 605
const handleStream = createStreamHandler({
mapResponse,
mapCompactResponse
mapCompactResponse,
streamOptions: {}
})
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.

@Aunali321
Copy link
Author

Hi SaltyAom,
Thanks for your open-source efforts. I came back to Elysia after a long time, and I think it's fair to say that it's been good enough to rival FastAPI for me.
If you think my motivation for this PR is good, then I'll fix the issues reported by CodeRabbit. If not, I'll close the PR. LMK your thoughts.

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.

1 participant