Skip to content

fix: stream end#302

Merged
killagu merged 9 commits intomasterfrom
fix-stream
Apr 18, 2025
Merged

fix: stream end#302
killagu merged 9 commits intomasterfrom
fix-stream

Conversation

@elrrrrrrr
Copy link
Contributor

@elrrrrrrr elrrrrrrr commented Apr 17, 2025

When the server returns a stream response, the context gets destroyed prematurely before the stream is fully consumed, which may lead to resource leaks or unexpected errors.

  1. Exported StreamUtil and added new untilEnd method to properly handle stream consumption
  2. Modified plugin/tegg/lib/ctx_lifecycle_middleware.ts to ensure complete consumption of response.body streams before context destruction

修复服务端返回 stream 时,stream 未消费完,上下文被错误释放

  • 导出 StreamUtil,新增 untilEnd 方法
  • 修改 plugin/tegg/lib/ctx_lifecycle_middleware.ts 当 response.body 为 Stream 时,确保消费完再出发 destory

Summary by CodeRabbit

  • New Features

    • Introduced a utility class for safer and more effective handling of Node.js streams.
    • Added new HTTP endpoints that stream HTML content asynchronously, including dynamic timestamps and background task information, with one endpoint simulating a streaming error.
  • Bug Fixes

    • Improved middleware to ensure proper cleanup after streaming responses, preventing premature resource destruction.
  • Tests

    • Added test cases to verify the streaming endpoints work correctly on supported Node.js versions.
  • Style

    • Minor formatting update in controller test fixture for improved readability.

@elrrrrrrr elrrrrrrr requested a review from killagu April 17, 2025 08:18
@coderabbitai
Copy link

coderabbitai bot commented Apr 17, 2025

"""

Walkthrough

A new utility class, StreamUtil, was introduced with a static method to detect Node.js streams and is now exported via the central utility barrel. The AppController in a test fixture was enhanced by injecting Tracer and BackgroundTaskHelper and adding new /stream and /error_stream HTTP endpoints that asynchronously stream HTML responses using async generators and Node.js Readable.from. Corresponding test cases were added to verify these streaming endpoints. The context lifecycle middleware was updated to defer context destruction until after streamed responses finish, using StreamUtil.isStream to detect streams and await-first to wait for the stream's 'close' or 'error' event before destroying the context. Minor formatting changes were made in unrelated test fixture files, and dependencies were updated accordingly.

Changes

File(s) Change Summary
core/common-util/index.ts Added export statement to re-export all exports from StreamUtil, making its utilities available via the barrel file.
core/common-util/src/StreamUtil.ts Introduced new StreamUtil class with static method isStream to detect if an object is a Node.js stream.
plugin/tegg/lib/ctx_lifecycle_middleware.ts Updated middleware to import StreamUtil and await-first. Deferred context destruction until after the response stream ends by checking if ctx.response.body is a stream and awaiting its 'close' or 'error' event before destroying context.
plugin/tegg/package.json Added new runtime dependencies "await-event": "^2.1.0" and "await-first": "^1.0.0".
plugin/controller/test/fixtures/apps/http-inject-app/app/controller/AppController.ts Enhanced AppController by injecting Tracer and BackgroundTaskHelper. Added new /stream and /error_stream endpoints that stream HTML responses asynchronously using async generators and Node.js Readable.from.
plugin/controller/test/http/request.test.ts Added new test cases "stream should work" and "error stream should work" verifying the /apps/stream and /apps/error_stream endpoints return 200 and include streamed content in the response text.
core/controller-decorator/test/fixtures/HTTPFooController.ts Added a blank line before the middleware1 function declaration. No functional changes.
plugin/controller/package.json Removed "globby" from devDependencies.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant AppController
    participant BackgroundTaskHelper
    participant ReadableStream
    participant Middleware

    Client->>AppController: HTTP GET /stream or /error_stream
    AppController->>BackgroundTaskHelper: (prepare data)
    AppController->>ReadableStream: Create stream from async generator
    AppController-->>Client: Streamed HTML response
    Middleware->>ReadableStream: Listen for 'close' or 'error' event
    ReadableStream-->>Middleware: Emits 'close' or 'error'
    Middleware->>Middleware: Destroy context after stream ends or errors
Loading

Possibly related issues

Poem

🐇
Streams now flow, not lost in haste,
With helpers new, no bytes go to waste.
Controllers serve with timed delight,
While middleware waits for the end of the night.
Tests now watch the river run—
Streaming responses, one by one!
🌊
"""

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

plugin/tegg/lib/ctx_lifecycle_middleware.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't find the plugin "eslint-plugin-eggache".

(The package "eslint-plugin-eggache" was not found when loaded as a Node module from the directory "".)

It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:

npm install eslint-plugin-eggache@latest --save-dev

The plugin "eslint-plugin-eggache" was referenced from the config file in ".eslintrc » eslint-config-egg/typescript » ./index.js".

If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 531132d and ec7ab6a.

📒 Files selected for processing (1)
  • plugin/tegg/lib/ctx_lifecycle_middleware.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugin/tegg/lib/ctx_lifecycle_middleware.ts
⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: Runner-ubuntu (20)
  • GitHub Check: Runner-ubuntu (22)
  • GitHub Check: Runner-ubuntu (16)
  • GitHub Check: Runner-macos (20)
  • GitHub Check: Runner-ubuntu (18)
  • GitHub Check: Runner-macos (16)
  • GitHub Check: Runner-macos (18)
  • GitHub Check: Analyze (typescript)
  • GitHub Check: Analyze (javascript)

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@elrrrrrrr elrrrrrrr added the bug Something isn't working label Apr 17, 2025
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 (5)
core/common-util/src/StreamUtil.ts (3)

8-14: Refactor static methods to use class name instead of 'this'

The static analysis tool flags the use of this in static methods as potentially confusing. Using the class name is more explicit.

Apply this refactor to make the code more maintainable:

  static isStream(obj: any): boolean {
    return obj !== null &&
      typeof obj === 'object' &&
      typeof obj.pipe === 'function' &&
      typeof obj.on === 'function' &&
      obj instanceof EventEmitter;
  }

  static isReadableStream(obj: any): boolean {
-    return this.isStream(obj) &&
+    return StreamUtil.isStream(obj) &&
      typeof (obj as Readable).read === 'function' &&
      typeof (obj as Readable).readable === 'boolean';
  }

Apply the same pattern to all other static methods.

Also applies to: 19-23, 28-32, 37-41, 46-50, 55-59, 70-74, 83-87, 124-128


135-141: Remove debug console.log statement

There's a console.log statement in the handleEnd function which should be removed before production.

  const handleEnd = () => {
-    console.log('handle end');
    stream.removeListener('end', handleEnd);
    stream.removeListener('error', handleEnd);
    stream.removeListener('close', handleEnd);
    callback();
  };

144-146: Consider using .once() instead of .on() for event handlers

Since each event should only trigger the callback once, using .once() is more appropriate and prevents potential memory leaks if the stream emits multiple events.

  // 添加事件监听器
-  stream.on('end', handleEnd);
-  stream.on('error', handleEnd);
-  stream.on('close', handleEnd);
+  stream.once('end', handleEnd);
+  stream.once('error', handleEnd);
+  stream.once('close', handleEnd);
plugin/tegg/lib/ctx_lifecycle_middleware.ts (1)

27-32: Function name has a typo: "doDestory" should be "doDestroy".

The implementation of the error handling is good, but the function name contains a spelling error.

-function doDestory() {
+function doDestroy() {

And remember to update the function name references in the code below.

plugin/controller/test/fixtures/apps/http-inject-app/app/controller/AppController.ts (1)

48-66: Good implementation of streaming HTTP response.

The stream implementation:

  1. Creates an async generator that yields HTML content in chunks with delays
  2. Sets appropriate headers for streaming content
  3. Properly uses Readable.from() to create a stream from the generator

Consider removing the console.log statement on line 54 unless it's needed for debugging purposes.

-        console.log('generate', i);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b0de0e and f7e6a83.

📒 Files selected for processing (6)
  • core/common-util/index.ts (1 hunks)
  • core/common-util/src/StreamUtil.ts (1 hunks)
  • core/controller-decorator/test/fixtures/HTTPFooController.ts (1 hunks)
  • plugin/controller/test/fixtures/apps/http-inject-app/app/controller/AppController.ts (3 hunks)
  • plugin/controller/test/http/request.test.ts (1 hunks)
  • plugin/tegg/lib/ctx_lifecycle_middleware.ts (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
plugin/tegg/lib/ctx_lifecycle_middleware.ts (1)
core/common-util/src/StreamUtil.ts (1)
  • StreamUtil (4-148)
plugin/controller/test/fixtures/apps/http-inject-app/app/controller/AppController.ts (2)
plugin/controller/test/fixtures/apps/http-inject-app/app/middleware/count_mw.ts (1)
  • countMw (6-9)
core/types/controller-decorator/model/types.ts (1)
  • EggContext (3-3)
🪛 Biome (1.9.4)
core/common-util/src/StreamUtil.ts

[error] 21-21: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 30-30: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 40-40: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 41-43: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 46-46: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 49-49: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 49-50: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 60-60: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 75-75: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 88-88: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 132-132: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

🪛 GitHub Check: Runner-macos (20)
core/common-util/src/StreamUtil.ts

[failure] 95-95:
'handleFinish' was used before it was defined

🪛 GitHub Check: Runner-ubuntu (20)
core/common-util/src/StreamUtil.ts

[failure] 95-95:
'handleFinish' was used before it was defined

🪛 GitHub Check: Runner-ubuntu (18)
core/common-util/src/StreamUtil.ts

[failure] 95-95:
'handleFinish' was used before it was defined

🪛 GitHub Check: Runner-macos (16)
core/common-util/src/StreamUtil.ts

[failure] 95-95:
'handleFinish' was used before it was defined

🪛 GitHub Check: Runner-macos (18)
core/common-util/src/StreamUtil.ts

[failure] 95-95:
'handleFinish' was used before it was defined

🪛 GitHub Check: Runner-ubuntu (16)
core/common-util/src/StreamUtil.ts

[failure] 95-95:
'handleFinish' was used before it was defined

🪛 GitHub Check: Runner-ubuntu (22)
core/common-util/src/StreamUtil.ts

[failure] 95-95:
'handleFinish' was used before it was defined

🪛 GitHub Actions: Continuous integration
core/common-util/src/StreamUtil.ts

[error] 95-95: ESLint: 'handleFinish' was used before it was defined. (@typescript-eslint/no-use-before-define)

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Analyze (typescript)
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (8)
core/controller-decorator/test/fixtures/HTTPFooController.ts (1)

9-9: Consistent vertical spacing

Adding this blank line improves code readability by providing consistent spacing between function definitions.

core/common-util/src/StreamUtil.ts (1)

1-148: New StreamUtil class implements comprehensive stream handling capabilities

This utility class provides robust methods for stream type checking, error handling, and lifecycle management - addressing the core issue in the PR where contexts are destroyed prematurely before streams are fully consumed.

🧰 Tools
🪛 Biome (1.9.4)

[error] 21-21: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 30-30: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 40-40: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 41-43: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 46-46: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 49-49: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 49-50: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 60-60: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 75-75: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 88-88: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 132-132: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

🪛 GitHub Check: Runner-macos (20)

[failure] 95-95:
'handleFinish' was used before it was defined

🪛 GitHub Check: Runner-ubuntu (20)

[failure] 95-95:
'handleFinish' was used before it was defined

🪛 GitHub Check: Runner-ubuntu (18)

[failure] 95-95:
'handleFinish' was used before it was defined

🪛 GitHub Check: Runner-macos (16)

[failure] 95-95:
'handleFinish' was used before it was defined

🪛 GitHub Check: Runner-macos (18)

[failure] 95-95:
'handleFinish' was used before it was defined

🪛 GitHub Check: Runner-ubuntu (16)

[failure] 95-95:
'handleFinish' was used before it was defined

🪛 GitHub Check: Runner-ubuntu (22)

[failure] 95-95:
'handleFinish' was used before it was defined

🪛 GitHub Actions: Continuous integration

[error] 95-95: ESLint: 'handleFinish' was used before it was defined. (@typescript-eslint/no-use-before-define)

core/common-util/index.ts (1)

12-12: LGTM: StreamUtil export added

The StreamUtil class is now properly exported from the barrel file, making it accessible throughout the project.

plugin/tegg/lib/ctx_lifecycle_middleware.ts (2)

4-4: Good addition of StreamUtil import.

This import is essential for implementing the fix to prevent premature context destruction when handling stream responses.


36-40: Great fix for stream handling.

This change correctly addresses the issue by deferring context destruction until after the stream is fully consumed. The implementation:

  1. Checks if the response body is a stream
  2. If it is, waits for the stream to end before destroying the context
  3. If not, destroys the context immediately

This prevents potential resource leaks and errors when streaming responses.

plugin/controller/test/fixtures/apps/http-inject-app/app/controller/AppController.ts (3)

2-2: Good addition of required imports.

The imports for setTimeout, Tracer, and Readable are appropriately added to support the new streaming functionality.

Also applies to: 15-16


13-13: Import statement updated correctly for Inject and BackgroundTaskHelper.

The necessary dependencies are properly imported to support the new functionality.


23-27: Proper use of dependency injection for required services.

The @Inject() decorator is used correctly to inject the required dependencies: tracer and backgroundTaskHelper.

Comment on lines +54 to +61
it('stream should work', async () => {
await app.httpRequest()
.get('/apps/stream')
.expect(200)
.expect(res => {
console.log(res.text);
});
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add assertions to the stream test

The test successfully verifies that the stream endpoint returns a 200 status code, but it would be more robust if it also verified some aspects of the response content beyond just logging it.

Consider enhancing the test with assertions:

  it('stream should work', async () => {
    await app.httpRequest()
      .get('/apps/stream')
      .expect(200)
      .expect(res => {
-        console.log(res.text);
+        assert(res.text.includes('<!DOCTYPE html>'), 'Response should contain HTML');
+        assert(res.text.length > 0, 'Response should not be empty');
      });
  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('stream should work', async () => {
await app.httpRequest()
.get('/apps/stream')
.expect(200)
.expect(res => {
console.log(res.text);
});
});
it('stream should work', async () => {
await app.httpRequest()
.get('/apps/stream')
.expect(200)
.expect(res => {
assert(res.text.includes('<!DOCTYPE html>'), 'Response should contain HTML');
assert(res.text.length > 0, 'Response should not be empty');
});
});

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: 1

🧹 Nitpick comments (2)
core/common-util/src/StreamUtil.ts (2)

19-41: Consider replacing this with the explicit class name to satisfy lint rule noThisInStatic.

All helpers use this.* inside static methods (e.g. this.isStream(obj)).
Biome flags this as “confusing” (noThisInStatic). While it works, swapping to the
explicit class (StreamUtil.isStream) quiets the warning and makes the call‑site
crystal‑clear to readers who may not realise this resolves to the constructor in
static context.

-    return this.isStream(obj) &&
+    return StreamUtil.isStream(obj) &&

You can apply the same substitution in isWritableStream, isDuplexStream,
isTransformStream, etc., or disable the rule locally (// biome-ignore lint/complexity/noThisInStatic) if you prefer the current style.

🧰 Tools
🪛 Biome (1.9.4)

[error] 21-21: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 30-30: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 40-40: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


124-147: Remove debug console.log and register listeners with .once.

  1. console.log('handle end') is a stray debug line that will pollute stdout in
    production.
  2. The method currently attaches three permanent listeners with stream.on.
    Although they are removed inside handleEnd, it is simpler and safer to use
    .once, letting Node handle automatic removal and eliminating any chance of
    duplicate callbacks if the stream emits multiple events in quick succession.
-    const handleEnd = () => {
-      console.log('handle end');
-      stream.removeListener('end', handleEnd);
-      stream.removeListener('error', handleEnd);
-      stream.removeListener('close', handleEnd);
-      callback();
-    };
-
-    // 添加事件监听器
-    stream.on('end', handleEnd);
-    stream.on('error', handleEnd);
-    stream.on('close', handleEnd);
+    const handleEnd = () => callback();
+
+    // 使用 once 确保只调用一次并自动清理监听器
+    stream.once('end', handleEnd);
+    stream.once('error', handleEnd);
+    stream.once('close', handleEnd);

These tweaks keep production logs clean and make the listener lifecycle more
robust.

🧰 Tools
🪛 Biome (1.9.4)

[error] 132-132: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f7e6a83 and 8e7710d.

📒 Files selected for processing (1)
  • core/common-util/src/StreamUtil.ts (1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
core/common-util/src/StreamUtil.ts

[error] 21-21: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 30-30: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 40-40: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 41-43: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 46-46: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 49-49: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 49-50: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 60-60: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 75-75: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 88-88: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 132-132: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: Analyze (typescript)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Runner-macos (20)
  • GitHub Check: Runner-ubuntu (16)
  • GitHub Check: Runner-ubuntu (22)
  • GitHub Check: Runner-ubuntu (20)
  • GitHub Check: Runner-ubuntu (18)
  • GitHub Check: Runner-macos (16)
  • GitHub Check: Runner-macos (18)

Comment on lines +60 to +65
const handler = errorHandler || ((error: Error) => {
console.error('Stream error:', error);
});

stream.on('error', handler);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Attach the error listener with .once to prevent accidental leak of handlers.

handleStreamError unconditionally registers an error listener with stream.on.
If a caller invokes this helper many times on the same stream (e.g. in a
framework’s middleware chain), each call adds another listener, risking the
Node.js “MaxListenersExceededWarning” and a small memory leak.

-    stream.on('error', handler);
+    stream.once('error', handler);

Using .once guarantees automatic removal after the first emission and removes
the burden from callers to clean up.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handler = errorHandler || ((error: Error) => {
console.error('Stream error:', error);
});
stream.on('error', handler);
}
const handler = errorHandler || ((error: Error) => {
console.error('Stream error:', error);
});
stream.once('error', handler);
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 60-60: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

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

♻️ Duplicate comments (1)
core/common-util/src/StreamUtil.ts (1)

31-33: Consider using .once() instead of .on() for event listeners.

Using .once() would automatically remove the listener after it fires once, which is safer and could prevent potential memory leaks if cleanup code doesn't execute for some reason.

-    stream.on('end', handleEnd);
-    stream.on('error', handleEnd);
-    stream.on('close', handleEnd);
+    stream.once('end', handleEnd);
+    stream.once('error', handleEnd);
+    stream.once('close', handleEnd);

Though your current implementation does manually remove listeners, using .once() provides an extra layer of safety.

🧹 Nitpick comments (2)
core/common-util/src/StreamUtil.ts (2)

4-35: Overall implementation is solid, but consider using namespaced functions instead of static class methods.

The StreamUtil implementation correctly handles stream detection and end-of-stream notification. However, the static analysis tool suggests using simple namespaced functions instead of a class with only static methods, which is generally considered a better practice in TypeScript/JavaScript.

-export class StreamUtil {
-  static isStream(obj: any): boolean {
+export const StreamUtil = {
+  isStream(obj: any): boolean {

This pattern makes the utility more aligned with functional programming principles while maintaining the same namespace organization.

🧰 Tools
🪛 Biome (1.9.4)

[error] 4-35: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)


[error] 14-14: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


18-18: Consider a more type-safe approach for checking the destroyed property.

Casting to any bypasses TypeScript's type checking. If the destroyed property is expected on certain stream types, consider using a more specific type or interface.

-    if ((stream as any).destroyed) {
+    // Option 1: Type guard approach
+    if ('destroyed' in stream && stream.destroyed) {

This approach maintains type safety while still checking for the property.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e7710d and 0f5f96c.

📒 Files selected for processing (1)
  • core/common-util/src/StreamUtil.ts (1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
core/common-util/src/StreamUtil.ts

[error] 4-35: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)


[error] 14-14: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: Runner-ubuntu (18)
  • GitHub Check: Runner-macos (18)
  • GitHub Check: Runner-ubuntu (16)
  • GitHub Check: Runner-macos (16)
  • GitHub Check: Runner-ubuntu (22)
  • GitHub Check: Runner-ubuntu (20)
  • GitHub Check: Runner-macos (20)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Analyze (typescript)
🔇 Additional comments (3)
core/common-util/src/StreamUtil.ts (3)

1-3: Well-structured imports using the Node.js protocol-style imports.

Good use of the node: prefix for Node.js core modules, which helps distinguish core modules from third-party ones.


5-11: Thorough stream detection implementation.

The isStream method properly checks for the essential characteristics of a Node.js stream (pipe method, on method, and EventEmitter inheritance).


23-29: Good cleanup implementation in the event handler.

The handler properly removes all listeners before executing the callback, which prevents memory leaks and ensures the callback is only called once.

@fengmk2
Copy link
Member

fengmk2 commented Apr 17, 2025

@coderabbitai 建一个 issue 跟进此 pr 会 pick 到 next 分支

@coderabbitai
Copy link

coderabbitai bot commented Apr 17, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Create a GitHub issue to track cherry-picking PR #302 to the next branch

# Create the issue with appropriate title, body, and assignee
gh issue create \
  --title "Cherry-pick PR #302 (fix: stream end) to next branch" \
  --body "## Description

This issue tracks the task of cherry-picking PR #302 to the 'next' branch.

## Reference
- Original PR: #302 (fix: stream end)

## Details
PR #302 fixes an issue where the server returns a stream response but the context is destroyed prematurely before the stream is fully consumed. The fix includes:
- Exporting a utility called StreamUtil and adding a new method named untilEnd
- Ensuring streams are fully consumed before context destruction

## Requested by
@fengmk2

## Task
- [ ] Cherry-pick PR #302 to the 'next' branch" \
  --assignee "fengmk2"

Length of output: 704


@fengmk2 我已经创建了一个用于跟进此 PR 会被 cherry-pick 到 next 分支的 issue。

Issue 已成功创建:#304

该 issue 包含了 PR #302 的相关信息,并已将您设为负责人。

(_/)
(•ᴥ•)
/ >📝 完成!

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: 1

♻️ Duplicate comments (1)
core/common-util/src/StreamUtil.ts (1)

9-9: Replace this with class name in static context.

Using this in a static context can be confusing as it refers to the class itself, not an instance. It's clearer to use the class name directly.

-    if (!this.isStream(stream)) {
+    if (!StreamUtil.isStream(stream)) {
🧰 Tools
🪛 Biome (1.9.4)

[error] 9-9: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

🧹 Nitpick comments (2)
core/common-util/src/StreamUtil.ts (2)

3-6: Consider using imported type-checking utilities instead of custom implementation.

Your isStream implementation is correct, but consider using established type-checking libraries like is-stream or the core Node.js types.isStream() utility for better maintainability and edge case handling.

-import { Stream } from 'node:stream';
+import { Stream } from 'node:stream';
+import isStream from 'is-stream';

 export class StreamUtil {
   static isStream(obj: any): boolean {
-    return obj instanceof Stream;
+    return isStream(obj);
   }
🧰 Tools
🪛 Biome (1.9.4)

[error] 3-29: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)


3-29: Consider refactoring from class to simple utility functions.

The static analysis suggests avoiding classes that contain only static members. Consider refactoring to simple exported functions for better maintainability.

 import { Stream } from 'node:stream';

-export class StreamUtil {
-  static isStream(obj: any): boolean {
-    return obj instanceof Stream;
-  }
+export function isStream(obj: any): boolean {
+  return obj instanceof Stream;
+}

-  static untilEnd(stream: Stream, callback: () => void): void {
-    if (!this.isStream(stream)) {
-      throw new Error('Invalid stream object');
-    }
+export function untilEnd(stream: Stream, callback: () => void): void {
+  if (!isStream(stream)) {
+    throw new Error('Invalid stream object');
+  }

-    if ((stream as any).destroyed) {
-      callback();
-      return;
-    }
+  if ((stream as any).destroyed) {
+    callback();
+    return;
+  }

-    const handleEnd = () => {
-      stream.removeListener('end', handleEnd);
-      stream.removeListener('error', handleEnd);
-      stream.removeListener('close', handleEnd);
-      callback();
-    };
+  const handleEnd = () => {
+    stream.removeListener('end', handleEnd);
+    stream.removeListener('error', handleEnd);
+    stream.removeListener('close', handleEnd);
+    callback();
+  };

-    stream.on('end', handleEnd);
-    stream.on('error', handleEnd);
-    stream.on('close', handleEnd);
-  }
-}
+  stream.once('end', handleEnd);
+  stream.once('error', handleEnd);
+  stream.once('close', handleEnd);
+}
🧰 Tools
🪛 Biome (1.9.4)

[error] 3-29: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)


[error] 9-9: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0f5f96c and 252a0ad.

📒 Files selected for processing (1)
  • core/common-util/src/StreamUtil.ts (1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
core/common-util/src/StreamUtil.ts

[error] 3-29: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)


[error] 9-9: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: Runner-ubuntu (20)
  • GitHub Check: Runner-macos (18)
  • GitHub Check: Runner-ubuntu (22)
  • GitHub Check: Runner-ubuntu (18)
  • GitHub Check: Runner-macos (20)
  • GitHub Check: Runner-ubuntu (16)
  • GitHub Check: Runner-macos (16)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Analyze (typescript)
🔇 Additional comments (2)
core/common-util/src/StreamUtil.ts (2)

13-16: LGTM! Good handling of already destroyed streams.

Checking for destroyed property and calling the callback immediately is a clean optimization that prevents unnecessary event handler registration.


18-23: LGTM! Proper event cleanup in handleEnd.

The handleEnd function correctly removes all listeners before executing the callback, which prevents memory leaks.

Comment on lines +25 to +27
stream.on('end', handleEnd);
stream.on('error', handleEnd);
stream.on('close', handleEnd);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use .once() instead of .on() for event listeners.

Using .on() can lead to memory leaks if untilEnd is called multiple times on the same stream. Since you intend for these handlers to execute only once, use .once() instead.

-    stream.on('end', handleEnd);
-    stream.on('error', handleEnd);
-    stream.on('close', handleEnd);
+    stream.once('end', handleEnd);
+    stream.once('error', handleEnd);
+    stream.once('close', handleEnd);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
stream.on('end', handleEnd);
stream.on('error', handleEnd);
stream.on('close', handleEnd);
stream.once('end', handleEnd);
stream.once('error', handleEnd);
stream.once('close', handleEnd);

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: 0

♻️ Duplicate comments (1)
core/common-util/src/StreamUtil.ts (1)

4-6: 💡 Verification agent

🧩 Analysis chain

Consider using an existing stream detection library.

A previous comment mentioned "is.stream" which suggests there might be existing utilities for this purpose. Consider using established libraries like is-stream npm package instead of implementing this functionality from scratch for better robustness and maintenance:

-import { Stream } from 'node:stream';
+import isStream from 'is-stream';

 export class StreamUtil {
   static isStream(obj: any): boolean {
-    return obj instanceof Stream;
+    return isStream(obj);
   }
 }

The is-stream package handles edge cases like stream-like objects that may not directly inherit from Node's Stream class.


🌐 Web query:

What is the is-stream npm package for Node.js and how does it detect streams?

💡 Result:

Overview of the is-stream npm Package

The is-stream npm package is a utility for Node.js that allows developers to check if a given value is a Node.js stream. It provides simple APIs to detect various stream types, such as generic streams, readable streams, writable streams, duplex streams, and transform streams[1].

Key Features

  • isStream(stream, options?): Returns a boolean indicating whether the input is any type of Node.js stream.
  • isWritableStream(stream, options?): Checks if the input is a writable stream (such as stream.Writable, http.OutgoingMessage, etc.).
  • isReadableStream(stream, options?): Checks if the input is a readable stream (e.g., stream.Readable, http.IncomingMessage).
  • isDuplexStream(stream, options?): Checks for duplex streams.
  • isTransformStream(stream, options?): Checks for transform streams.
  • Options: You can pass an option called checkOpen (default: true), which, when set, causes the function to return false if the stream has already been closed[1].

Usage Example

import fs from 'node:fs';
import { isStream } from 'is-stream';

isStream(fs.createReadStream('unicorn.png')); // => true
isStream({}); // => false

This detects whether the object passed is a stream[1].

How Does is-stream Detect Streams?

Detection is broadly based on checking for characteristic properties and functions associated with Node.js streams. Typically, streams in Node.js are objects that inherit from the EventEmitter class and implement methods such as pipe[3][4][8][10].

Common checks include:

  • The value is an object and not null.
  • The value has a pipe method that is a function, which is a strong indicator of a stream in Node.js[10].
  • For specific stream types:
    • Readable streams: Must have a _read function and a _readableState object in addition to a pipe method[10].
    • Writable streams: Must have a _write function and a _writableState object, plus a pipe method[10].
    • Duplex streams: Must qualify as both readable and writable[10].

Here’s an abstracted example based on standard detection logic:

// Generic stream check
const isStream = val =>
  val !== null && typeof val === 'object' && typeof val.pipe === 'function';

// Readable stream check
const isReadableStream = val =>
  isStream(val) &&
  typeof val._read === 'function' &&
  typeof val._readableState === 'object';

// Writable stream check
const isWritableStream = val =>
  isStream(val) &&
  typeof val._write === 'function' &&
  typeof val._writableState === 'object';

[10]

Summary Table

Function Detects Key Properties Checked
isStream Any Node.js stream object, .pipe function
isReadableStream Readable stream .pipe, _read function, _readableState object
isWritableStream Writable stream .pipe, _write function, _writableState object
isDuplexStream Duplex stream (readable & writable) All of readable and writable checks
isTransformStream Transform stream (duplex with transform) Further internal checks (not shown)

Conclusion

The is-stream package provides a practical and reliable way to detect various types of Node.js streams by checking for the presence of essential properties and methods commonly implemented by Node.js stream classes[1][10]. This helps avoid subtle bugs and enables more robust handling of stream-based data processing.

Citations:


Use is-stream for more comprehensive stream detection

Our current instanceof Stream check only recognizes direct node:stream instances and can miss stream-like objects. The is-stream package reliably detects all Node.js stream types (readable, writable, duplex, transform) by checking for characteristic methods and state, including handling closed streams via its checkOpen option.

  • File to update: core/common-util/src/StreamUtil.ts
    • Replace the Stream import with is-stream
    • Update the isStream method to delegate to isStream(obj)
- import { Stream } from 'node:stream';
+ import isStream from 'is-stream';

 export class StreamUtil {
   static isStream(obj: any): boolean {
-    return obj instanceof Stream;
+    return isStream(obj);
   }
 }
🧹 Nitpick comments (1)
core/common-util/src/StreamUtil.ts (1)

3-7: Consider using a simple function instead of a class with only static members.

The StreamUtil class contains only static members which is flagged by static analysis. For better maintainability and adherence to functional programming principles, consider refactoring to use a simple function:

-export class StreamUtil {
-  static isStream(obj: any): boolean {
-    return obj instanceof Stream;
-  }
-}
+export function isStream(obj: any): boolean {
+  return obj instanceof Stream;
+}

If you need to keep the namespace for organization, you could use:

-export class StreamUtil {
-  static isStream(obj: any): boolean {
-    return obj instanceof Stream;
-  }
-}
+export const StreamUtil = {
+  isStream(obj: any): boolean {
+    return obj instanceof Stream;
+  }
+};
🧰 Tools
🪛 Biome (1.9.4)

[error] 3-7: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 252a0ad and 1d24817.

📒 Files selected for processing (4)
  • core/common-util/src/StreamUtil.ts (1 hunks)
  • plugin/controller/test/http/request.test.ts (1 hunks)
  • plugin/tegg/lib/ctx_lifecycle_middleware.ts (2 hunks)
  • plugin/tegg/package.json (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • plugin/tegg/package.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • plugin/controller/test/http/request.test.ts
  • plugin/tegg/lib/ctx_lifecycle_middleware.ts
🧰 Additional context used
🪛 Biome (1.9.4)
core/common-util/src/StreamUtil.ts

[error] 3-7: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)

⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Runner-ubuntu (20)
  • GitHub Check: Runner-ubuntu (16)
  • GitHub Check: Runner-ubuntu (22)
  • GitHub Check: Analyze (typescript)
  • GitHub Check: Runner-ubuntu (18)
  • GitHub Check: Runner-macos (20)
  • GitHub Check: Runner-macos (18)
  • GitHub Check: Runner-macos (16)
🔇 Additional comments (1)
core/common-util/src/StreamUtil.ts (1)

1-7: LGTM! Simple and focused utility implementation.

The implementation is straightforward and correctly identifies Node.js Stream instances. This utility serves the PR's purpose well by enabling the detection of stream responses so the context isn't destroyed prematurely.

🧰 Tools
🪛 Biome (1.9.4)

[error] 3-7: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)

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 (3)
core/common-util/src/StreamUtil.ts (2)

3-7: Consider using a simple exported function instead of a class with only static methods

The static analysis correctly points out that classes with only static methods can be simplified to exported functions, which aligns with modern TypeScript best practices.

-export class StreamUtil {
-  static isStream(obj: any): boolean {
-    return obj instanceof Stream;
-  }
-}
+export function isStream(obj: any): boolean {
+  return obj instanceof Stream;
+}

If you prefer to keep the namespace for organizational purposes, you could use:

export const StreamUtil = {
  isStream(obj: any): boolean {
    return obj instanceof Stream;
  }
};
🧰 Tools
🪛 Biome (1.9.4)

[error] 3-7: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)


4-6: Consider using a more robust stream detection approach

While instanceof Stream works for Node.js native streams, it might not correctly identify stream-like objects that implement the stream interface but don't directly inherit from Stream (like some third-party libraries).

You could consider:

  1. Using an established utility like is-stream npm package
  2. Checking for stream-like behavior (presence of pipe method)
-  static isStream(obj: any): boolean {
-    return obj instanceof Stream;
-  }
+  static isStream(obj: any): boolean {
+    return obj !== null && 
+           typeof obj === 'object' && 
+           (obj instanceof Stream || 
+            typeof obj.pipe === 'function');
+  }
plugin/tegg/lib/ctx_lifecycle_middleware.ts (1)

28-33: Fix typo in function name: "doDestory" → "doDestroy"

There's a typo in the function name which should be fixed for code clarity.

-function doDestory() {
+function doDestroy() {

Also apply this change to the function calls at lines 38 and 40.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1d24817 and fbdeac4.

📒 Files selected for processing (4)
  • core/common-util/src/StreamUtil.ts (1 hunks)
  • plugin/controller/test/http/request.test.ts (1 hunks)
  • plugin/tegg/lib/ctx_lifecycle_middleware.ts (2 hunks)
  • plugin/tegg/package.json (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • plugin/tegg/package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugin/controller/test/http/request.test.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
plugin/tegg/lib/ctx_lifecycle_middleware.ts (1)
core/common-util/src/StreamUtil.ts (1)
  • StreamUtil (3-7)
🪛 Biome (1.9.4)
core/common-util/src/StreamUtil.ts

[error] 3-7: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)

⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: Analyze (typescript)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Runner-ubuntu (20)
  • GitHub Check: Runner-ubuntu (22)
  • GitHub Check: Runner-ubuntu (18)
  • GitHub Check: Runner-ubuntu (16)
  • GitHub Check: Runner-macos (16)
  • GitHub Check: Runner-macos (20)
  • GitHub Check: Runner-macos (18)
🔇 Additional comments (1)
plugin/tegg/lib/ctx_lifecycle_middleware.ts (1)

37-41: Verify middleware behavior when asynchronously destroying context

The middleware completes without awaiting the completion of the stream events and context destruction. This means the response might be sent to the client with clean-up operations still pending.

Is this intentional? If not, consider one of these approaches:

  1. Make the middleware wait for all cleanup to complete (more resource-intensive)
  2. Document that cleanup happens asynchronously after the response is sent (current behavior)

If option 1 is preferred, you could implement:

-    if (StreamUtil.isStream(ctx.response.body)) {
-      awaitEvent(ctx.response.body, 'close').then(doDestory);
-    } else {
-      doDestory();
-    }
+    if (StreamUtil.isStream(ctx.response.body)) {
+      try {
+        await awaitEvent(ctx.response.body, 'close');
+      } catch (e) {
+        ctx.logger.error('[tegg/ctxLifecycleMiddleware] error while waiting for stream close event:', e);
+      }
+    }
+    doDestory();

Comment on lines +37 to +39
if (StreamUtil.isStream(ctx.response.body)) {
awaitEvent(ctx.response.body, 'close').then(doDestory);
} else {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider listening for other relevant stream events

Currently, the code only waits for the 'close' event, but depending on the stream type, other events might signal completion first. For better handling of different stream types, consider listening for multiple events like 'end' and 'finish' as well.

-    if (StreamUtil.isStream(ctx.response.body)) {
-      awaitEvent(ctx.response.body, 'close').then(doDestory);
+    if (StreamUtil.isStream(ctx.response.body)) {
+      // Different stream types may emit different events to signal completion
+      // - Readable streams emit 'end' when data flow is complete
+      // - Writable streams emit 'finish' when all data is written
+      // - All streams emit 'close' when the underlying resource is closed
+      const stream = ctx.response.body;
+      Promise.race([
+        awaitEvent(stream, 'close'),
+        awaitEvent(stream, 'end'),
+        awaitEvent(stream, 'finish'),
+      ]).then(doDestory);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (StreamUtil.isStream(ctx.response.body)) {
awaitEvent(ctx.response.body, 'close').then(doDestory);
} else {
if (StreamUtil.isStream(ctx.response.body)) {
// Different stream types may emit different events to signal completion
// - Readable streams emit 'end' when data flow is complete
// - Writable streams emit 'finish' when all data is written
// - All streams emit 'close' when the underlying resource is closed
const stream = ctx.response.body;
Promise.race([
awaitEvent(stream, 'close'),
awaitEvent(stream, 'end'),
awaitEvent(stream, 'finish'),
]).then(doDestory);
} else {

🛠️ Refactor suggestion

Add error handling for the stream event promise

The current implementation doesn't handle errors that might occur while waiting for the stream event. This could lead to silent failures if the stream encounters an error.

-    if (StreamUtil.isStream(ctx.response.body)) {
-      awaitEvent(ctx.response.body, 'close').then(doDestory);
+    if (StreamUtil.isStream(ctx.response.body)) {
+      awaitEvent(ctx.response.body, 'close')
+        .then(doDestory)
+        .catch(e => {
+          e.message = '[tegg/ctxLifecycleMiddleware] error while waiting for stream close event: ' + e.message;
+          ctx.logger.error(e);
+          doDestory(); // Still attempt to destroy the context
+        });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (StreamUtil.isStream(ctx.response.body)) {
awaitEvent(ctx.response.body, 'close').then(doDestory);
} else {
if (StreamUtil.isStream(ctx.response.body)) {
awaitEvent(ctx.response.body, 'close')
.then(doDestory)
.catch(e => {
e.message = '[tegg/ctxLifecycleMiddleware] error while waiting for stream close event: ' + e.message;
ctx.logger.error(e);
doDestory(); // Still attempt to destroy the context
});
} else {

Copy link
Contributor

@killagu killagu left a comment

Choose a reason for hiding this comment

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

需要补充异常 case

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

♻️ Duplicate comments (1)
plugin/tegg/lib/ctx_lifecycle_middleware.ts (1)

29-35: Consider listening for additional stream events

Currently, the code only listens for 'close' and 'error' events, but depending on the stream type, other events might signal completion first. For better handling of different stream types, consider listening for 'end' and 'finish' events as well.

     if (StreamUtil.isStream(ctx.response.body)) {
       try {
-        await awaitFirst(ctx.response.body, [ 'close', 'error' ]);
+        await awaitFirst(ctx.response.body, [ 'close', 'error', 'end', 'finish' ]);
       } catch (error) {
         ctx.res.destroy(error);
       }
     }
🧹 Nitpick comments (2)
plugin/controller/test/fixtures/apps/http-inject-app/app/controller/AppController.ts (2)

48-66: Well-implemented streaming response

The implementation of the streaming response is well structured. A few observations:

  1. Setting X-Accel-Buffering to 'no' correctly disables Nginx buffering if used
  2. Using Readable.from to create a stream from the async generator is appropriate
  3. The console.log statement might be intended for debugging but consider removing it or replacing with proper logging
     async function* generate(count = 5, duration = 500) {
       yield '<html><head><title>hello stream</title></head><body>';
       for (let i = 0; i < count; i++) {
-        console.log('generate', i);
+        // Use a logger instead of console.log for production code
         yield `<h2>流式内容${i + 1},${Date()}</h2>`;
         await setTimeout(duration);
       }
       yield self.backgroundTaskHelper.toString();
       yield '</body></html>';
     }

68-89: Well-implemented error stream for testing

The error stream implementation is a good way to test error handling in streams. Same suggestion about console.log applies here.

     async function* generate(count = 5, duration = 500) {
       yield '<html><head><title>hello stream</title></head><body>';
       for (let i = 0; i < count; i++) {
-        console.log('generate', i);
+        // Use a logger instead of console.log for production code
         if (i > 2) {
           throw new Error('test');
         }
         yield `<h2>流式内容${i + 1},${Date()}</h2>`;
         await setTimeout(duration);
       }
       yield self.backgroundTaskHelper.toString();
       yield '</body></html>';
     }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4edc0fd and f02c858.

📒 Files selected for processing (5)
  • plugin/controller/package.json (0 hunks)
  • plugin/controller/test/fixtures/apps/http-inject-app/app/controller/AppController.ts (3 hunks)
  • plugin/controller/test/http/request.test.ts (1 hunks)
  • plugin/tegg/lib/ctx_lifecycle_middleware.ts (2 hunks)
  • plugin/tegg/package.json (1 hunks)
💤 Files with no reviewable changes (1)
  • plugin/controller/package.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • plugin/tegg/package.json
  • plugin/controller/test/http/request.test.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
plugin/tegg/lib/ctx_lifecycle_middleware.ts (2)
core/common-util/src/StreamUtil.ts (1)
  • StreamUtil (3-7)
core/eventbus-runtime/src/SingletonEventBus.ts (1)
  • awaitFirst (61-63)
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: Runner-macos (20)
  • GitHub Check: Runner-ubuntu (20)
  • GitHub Check: Runner-ubuntu (18)
  • GitHub Check: Runner-ubuntu (22)
  • GitHub Check: Runner-ubuntu (16)
🔇 Additional comments (3)
plugin/tegg/lib/ctx_lifecycle_middleware.ts (1)

4-5: Import necessary dependencies for stream handling

Good additions of the StreamUtil from '@eggjs/tegg-common-util' and awaitFirst from 'await-first' to handle stream events properly.

plugin/controller/test/fixtures/apps/http-inject-app/app/controller/AppController.ts (2)

13-16: Good additions of required imports

The imports added for dependency injection and stream handling are appropriate for the new functionality.


23-27: Properly injected dependencies

The @Inject() decorators are properly used to inject the required dependencies.

ctx.logger.error(e);
});
}
doDestory();
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Update function call to match name

If you update the function name from doDestory to doDestroy as suggested above, remember to update the function call here as well.

-    doDestory();
+    doDestroy();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
doDestory();
doDestroy();

Copy link
Contributor

@killagu killagu left a comment

Choose a reason for hiding this comment

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

LGTM

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: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f02c858 and c7e38d5.

📒 Files selected for processing (1)
  • plugin/controller/test/http/request.test.ts (1 hunks)
🧰 Additional context used
🪛 GitHub Check: Runner-macos (18)
plugin/controller/test/http/request.test.ts

[failure] 68-68:
Expected indentation of 8 spaces but found 6

🪛 GitHub Check: Runner-macos (16)
plugin/controller/test/http/request.test.ts

[failure] 68-68:
Expected indentation of 8 spaces but found 6

🪛 GitHub Check: Runner-ubuntu (22)
plugin/controller/test/http/request.test.ts

[failure] 68-68:
Expected indentation of 8 spaces but found 6

🪛 GitHub Check: Runner-macos (20)
plugin/controller/test/http/request.test.ts

[failure] 68-68:
Expected indentation of 8 spaces but found 6

🪛 GitHub Check: Runner-ubuntu (18)
plugin/controller/test/http/request.test.ts

[failure] 68-68:
Expected indentation of 8 spaces but found 6

🪛 GitHub Check: Runner-ubuntu (16)
plugin/controller/test/http/request.test.ts

[failure] 68-68:
Expected indentation of 8 spaces but found 6

🪛 GitHub Check: Runner-ubuntu (20)
plugin/controller/test/http/request.test.ts

[failure] 68-68:
Expected indentation of 8 spaces but found 6

🪛 GitHub Actions: Continuous integration
plugin/controller/test/http/request.test.ts

[error] 68-68: Expected indentation of 8 spaces but found 6 (@typescript-eslint/indent)

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Analyze (typescript)
🔇 Additional comments (1)
plugin/controller/test/http/request.test.ts (1)

54-61: Good implementation of stream test with assertion

The test successfully verifies that the stream endpoint returns a 200 status code and checks for expected content in the response. This provides proper validation of the streaming functionality.

Copy link
Contributor

@killagu killagu left a comment

Choose a reason for hiding this comment

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

LGTM

@killagu killagu merged commit 7f1f4b3 into master Apr 18, 2025
13 checks passed
@killagu killagu deleted the fix-stream branch April 18, 2025 08:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants