Repository: eggjs/tegg
Pull request #302: fix: stream end
Author: login: elrrrrrrr
Labels: name: bug
description: Something isn't working
Created: 2025-04-17T08:18:32Z | Merged: 2025-04-18T08:28:45Z

> 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

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/core/common-util/index.ts b/core/common-util/index.ts
index d75acb7bb..4229fb4b5 100644
--- a/core/common-util/index.ts
+++ b/core/common-util/index.ts
@@ -9,3 +9,4 @@ export * from './src/ProxyUtil';
 export * from './src/ModuleConfig';
 export * from './src/ModuleConfigs';
 export * from './src/TimerUtil';
+export * from './src/StreamUtil';
diff --git a/core/common-util/src/StreamUtil.ts b/core/common-util/src/StreamUtil.ts
new file mode 100644
index 000000000..33578ebe6
--- /dev/null
+++ b/core/common-util/src/StreamUtil.ts
@@ -0,0 +1,7 @@
+import { Stream } from 'node:stream';
+
+export class StreamUtil {
+  static isStream(obj: any): boolean {
+    return obj instanceof Stream;
+  }
+}
diff --git a/core/controller-decorator/test/fixtures/HTTPFooController.ts b/core/controller-decorator/test/fixtures/HTTPFooController.ts
index 6930853d1..ef4e25eeb 100644
--- a/core/controller-decorator/test/fixtures/HTTPFooController.ts
+++ b/core/controller-decorator/test/fixtures/HTTPFooController.ts
@@ -6,6 +6,7 @@ import { Middleware } from '../../src/decorator/Middleware';
 import { HTTPBody, HTTPParam, HTTPQueries, HTTPQuery, HTTPHeaders } from '../../src/decorator/http/HTTPParam';
 import { HTTPMethod } from '../../src/decorator/http/HTTPMethod';
 
+
 async function middleware1(ctx: EggContext, next: Next) {
   console.log(ctx, next);
 }
diff --git a/plugin/controller/package.json b/plugin/controller/package.json
index 8e954de43..c95a81ae3 100644
--- a/plugin/controller/package.json
+++ b/plugin/controller/package.json
@@ -69,7 +69,6 @@
     "egg": "^3.9.1",
     "egg-mock": "^5.5.0",
     "egg-tracer": "^2.0.0",
-    "globby": "^11.1.0",
     "mocha": "^10.2.0",
     "ts-node": "^10.9.1",
     "typescript": "^5.0.4"
diff --git a/plugin/controller/test/fixtures/apps/http-inject-app/app/controller/AppController.ts b/plugin/controller/test/fixtures/apps/http-inject-app/app/controller/AppController.ts
index 81ae65dc5..c5a89616d 100644
--- a/plugin/controller/test/fixtures/apps/http-inject-app/app/controller/AppController.ts
+++ b/plugin/controller/test/fixtures/apps/http-inject-app/app/controller/AppController.ts
@@ -1,4 +1,5 @@
 import { Context as EggContext } from 'egg';
+import { setTimeout } from 'node:timers/promises';
 import {
   Context,
   HTTPController,
@@ -9,20 +10,28 @@ import {
   HTTPRequest,
   Cookies,
   HTTPCookies,
-} from '@eggjs/tegg';
+  Inject, BackgroundTaskHelper } from '@eggjs/tegg';
 import { countMw } from '../middleware/count_mw';
+import { Tracer } from '@eggjs/module-test-util';
+import { Readable } from 'node:stream';
 
 @HTTPController({
   path: '/apps',
 })
 @Middleware(countMw)
 export class AppController {
+  @Inject()
+  readonly tracer: Tracer;
+
+  @Inject()
+  readonly backgroundTaskHelper: BackgroundTaskHelper;
 
   @HTTPMethod({
     method: HTTPMethodEnum.POST,
     path: '/testRequest',
   })
 
+
   async testRequest(@Context() ctx: EggContext, @Request() request: HTTPRequest, @Cookies() cookies: HTTPCookies) {
     const traceId = await ctx.tracer.traceId;
     return {
@@ -34,4 +43,48 @@ export class AppController {
       cookies: cookies.get('test', { signed: false }),
     };
   }
+
+
+  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/stream' })
+  async streamResponse(@Context() ctx: EggContext) {
+    const self = this;
+    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);
+        yield `<h2>流式内容${i + 1}，${Date()}</h2>`;
+        await setTimeout(duration);
+      }
+      yield self.backgroundTaskHelper.toString();
+      yield '</body></html>';
+
+    }
+
+    ctx.type = 'html';
+    ctx.set('X-Accel-Buffering', 'no');
+    return Readable.from(generate());
+  }
+
+  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/error_stream' })
+  async errorStreamResponse(@Context() ctx: EggContext) {
+    const self = this;
+    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);
+        if (i > 2) {
+          throw new Error('test');
+        }
+        yield `<h2>流式内容${i + 1}，${Date()}</h2>`;
+        await setTimeout(duration);
+      }
+      yield self.backgroundTaskHelper.toString();
+      yield '</body></html>';
+
+    }
+
+    ctx.type = 'html';
+    ctx.set('X-Accel-Buffering', 'no');
+    return Readable.from(generate());
+  }
 }
diff --git a/plugin/controller/test/http/request.test.ts b/plugin/controller/test/http/request.test.ts
index 16bd66400..d47016780 100644
--- a/plugin/controller/test/http/request.test.ts
+++ b/plugin/controller/test/http/request.test.ts
@@ -50,6 +50,23 @@ describe('plugin/controller/test/http/request.test.ts', () => {
           assert(res.body.cookies === 'foo');
         });
     });
+
+    it('stream should work', async () => {
+      await app.httpRequest()
+        .get('/apps/stream')
+        .expect(200)
+        .expect(res => {
+          assert(res.text.includes('流式内容5'));
+        });
+    });
+
+    it('error stream should work', async () => {
+      await assert.rejects(
+        app.httpRequest()
+          .get('/apps/error_stream')
+          .expect(200)
+        , /Error: aborted/);
+    });
   }
 
 });
diff --git a/plugin/tegg/lib/ctx_lifecycle_middleware.ts b/plugin/tegg/lib/ctx_lifecycle_middleware.ts
index 4171e2f63..b6594b282 100644
--- a/plugin/tegg/lib/ctx_lifecycle_middleware.ts
+++ b/plugin/tegg/lib/ctx_lifecycle_middleware.ts
@@ -1,6 +1,8 @@
 import { TEggPluginContext } from '../app/extend/context';
 import { EggContextImpl } from './EggContextImpl';
 import { ROOT_PROTO, TEGG_CONTEXT } from '@eggjs/egg-module-common';
+import { StreamUtil } from '@eggjs/tegg-common-util';
+import awaitFirst from 'await-first';
 
 export default async function ctxLifecycleMiddleware(ctx: TEggPluginContext, next) {
   // should not recreate teggContext
@@ -22,14 +24,27 @@ export default async function ctxLifecycleMiddleware(ctx: TEggPluginContext, nex
   if (teggCtx.init) {
     await teggCtx.init(lifecycle);
   }
+
+  async function doDestory() {
+    if (StreamUtil.isStream(ctx.response.body)) {
+      try {
+        await awaitFirst(ctx.response.body, [ 'close', 'error' ]);
+      } catch (error) {
+        ctx.res.destroy(error);
+      }
+    }
+    try {
+      if (teggCtx.destroy) {
+        await teggCtx.destroy(lifecycle);
+      }
+    } catch (e) {
+      e.message = '[tegg/ctxLifecycleMiddleware] destroy tegg ctx failed:' + e.message;
+      ctx.logger.error(e);
+    }
+  }
   try {
     await next();
   } finally {
-    if (teggCtx.destroy) {
-      teggCtx.destroy(lifecycle).catch(e => {
-        e.message = '[tegg/ctxLifecycleMiddleware] destroy tegg ctx failed:' + e.message;
-        ctx.logger.error(e);
-      });
-    }
+    doDestory();
   }
 }
diff --git a/plugin/tegg/package.json b/plugin/tegg/package.json
index 4a7926b0b..2f48310bf 100644
--- a/plugin/tegg/package.json
+++ b/plugin/tegg/package.json
@@ -53,6 +53,8 @@
     "@eggjs/tegg-loader": "^3.52.1",
     "@eggjs/tegg-metadata": "^3.52.1",
     "@eggjs/tegg-runtime": "^3.52.1",
+    "await-event": "^2.1.0",
+    "await-first": "^1.0.0",
     "extend2": "^1.0.0",
     "sdk-base": "^4.2.0"
   },


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>core/common-util/src/StreamUtil.ts (3)</summary><blockquote>

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

```diff
  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.


```diff
  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.


```diff
  // 添加事件监听器
-  stream.on('end', handleEnd);
-  stream.on('error', handleEnd);
-  stream.on('close', handleEnd);
+  stream.once('end', handleEnd);
+  stream.once('error', handleEnd);
+  stream.once('close', handleEnd);
```

</blockquote></details>
<details>
<summary>plugin/tegg/lib/ctx_lifecycle_middleware.ts (1)</summary><blockquote>

`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.

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

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

</blockquote></details>
<details>
<summary>plugin/controller/test/fixtures/apps/http-inject-app/app/controller/AppController.ts (1)</summary><blockquote>

`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.


```diff
-        console.log('generate', i);
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

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

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 5b0de0e7689546a466cfce8bd7ec12e9721c301e and f7e6a832b14c3cf46ab60c5bb6a2bf2c22dd3986.

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `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)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>plugin/tegg/lib/ctx_lifecycle_middleware.ts (1)</summary><blockquote>

<details>
<summary>core/common-util/src/StreamUtil.ts (1)</summary>

* `StreamUtil` (4-148)

</details>

</blockquote></details>
<details>
<summary>plugin/controller/test/fixtures/apps/http-inject-app/app/controller/AppController.ts (2)</summary><blockquote>

<details>
<summary>plugin/controller/test/fixtures/apps/http-inject-app/app/middleware/count_mw.ts (1)</summary>

* `countMw` (6-9)

</details>
<details>
<summary>core/types/controller-decorator/model/types.ts (1)</summary>

* `EggContext` (3-3)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>core/common-util/src/StreamUtil.ts</summary>

[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)

</details>

</details>
<details>
<summary>🪛 GitHub Check: Runner-macos (20)</summary>

<details>
<summary>core/common-util/src/StreamUtil.ts</summary>

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

</details>

</details>
<details>
<summary>🪛 GitHub Check: Runner-ubuntu (20)</summary>

<details>
<summary>core/common-util/src/StreamUtil.ts</summary>

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

</details>

</details>
<details>
<summary>🪛 GitHub Check: Runner-ubuntu (18)</summary>

<details>
<summary>core/common-util/src/StreamUtil.ts</summary>

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

</details>

</details>
<details>
<summary>🪛 GitHub Check: Runner-macos (16)</summary>

<details>
<summary>core/common-util/src/StreamUtil.ts</summary>

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

</details>

</details>
<details>
<summary>🪛 GitHub Check: Runner-macos (18)</summary>

<details>
<summary>core/common-util/src/StreamUtil.ts</summary>

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

</details>

</details>
<details>
<summary>🪛 GitHub Check: Runner-ubuntu (16)</summary>

<details>
<summary>core/common-util/src/StreamUtil.ts</summary>

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

</details>

</details>
<details>
<summary>🪛 GitHub Check: Runner-ubuntu (22)</summary>

<details>
<summary>core/common-util/src/StreamUtil.ts</summary>

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

</details>

</details>
<details>
<summary>🪛 GitHub Actions: Continuous integration</summary>

<details>
<summary>core/common-util/src/StreamUtil.ts</summary>

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

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: Analyze (typescript)
* GitHub Check: Analyze (javascript)

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>core/controller-decorator/test/fixtures/HTTPFooController.ts (1)</summary>

`9-9`: **Consistent vertical spacing**

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

</details>
<details>
<summary>core/common-util/src/StreamUtil.ts (1)</summary>

`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.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[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)

</details>
<details>
<summary>🪛 GitHub Check: Runner-macos (20)</summary>

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

</details>
<details>
<summary>🪛 GitHub Check: Runner-ubuntu (20)</summary>

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

</details>
<details>
<summary>🪛 GitHub Check: Runner-ubuntu (18)</summary>

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

</details>
<details>
<summary>🪛 GitHub Check: Runner-macos (16)</summary>

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

</details>
<details>
<summary>🪛 GitHub Check: Runner-macos (18)</summary>

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

</details>
<details>
<summary>🪛 GitHub Check: Runner-ubuntu (16)</summary>

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

</details>
<details>
<summary>🪛 GitHub Check: Runner-ubuntu (22)</summary>

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

</details>
<details>
<summary>🪛 GitHub Actions: Continuous integration</summary>

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

</details>

</details>

</details>
<details>
<summary>core/common-util/index.ts (1)</summary>

`12-12`: **LGTM: StreamUtil export added**

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

</details>
<details>
<summary>plugin/tegg/lib/ctx_lifecycle_middleware.ts (2)</summary>

`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.

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

`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`.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-17T08:21:36Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>core/common-util/src/StreamUtil.ts (2)</summary><blockquote>

`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.

```diff
-    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.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[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)

</details>

</details>

---

`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.

```diff
-    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.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[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)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

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

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between f7e6a832b14c3cf46ab60c5bb6a2bf2c22dd3986 and 8e7710d96a37dd827202e4e3e29233641d807406.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

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

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>core/common-util/src/StreamUtil.ts</summary>

[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)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (9)</summary>

* 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)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-17T08:25:16Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>core/common-util/src/StreamUtil.ts (1)</summary><blockquote>

`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.

```diff
-    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.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>core/common-util/src/StreamUtil.ts (2)</summary><blockquote>

`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.

```diff
-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.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[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)

</details>

</details>

---

`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.

```diff
-    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.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

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

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8e7710d96a37dd827202e4e3e29233641d807406 and 0f5f96c0a60daed2a75322ad1aa33e66db67a639.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

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

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>core/common-util/src/StreamUtil.ts</summary>

[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)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (9)</summary>

* 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)

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>core/common-util/src/StreamUtil.ts (3)</summary>

`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.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-17T09:44:42Z
user: login: killagu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T11:42:37Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>core/common-util/src/StreamUtil.ts (1)</summary><blockquote>

`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.

```diff
-    if (!this.isStream(stream)) {
+    if (!StreamUtil.isStream(stream)) {
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[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)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>core/common-util/src/StreamUtil.ts (2)</summary><blockquote>

`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.

```diff
-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);
   }
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

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

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

(lint/complexity/noStaticOnlyClass)

</details>

</details>

---

`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.

```diff
 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);
+}
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[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)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

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

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 0f5f96c0a60daed2a75322ad1aa33e66db67a639 and 252a0adf5525c6ccfb3a9213cb70c54a07a5c142.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

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

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>core/common-util/src/StreamUtil.ts</summary>

[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)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (9)</summary>

* 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)

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>core/common-util/src/StreamUtil.ts (2)</summary>

`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.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-17T11:46:53Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>core/common-util/src/StreamUtil.ts (1)</summary><blockquote>

`4-6`: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

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

```diff
-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:

```web
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

```js
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:

```js
// 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:

- 1: https://www.npmjs.com/package/is-stream
- 2: https://www.npmjs.com/package/stream
- 3: https://nodejs.org/api/stream.html
- 4: http://nodesource.com/blog/understanding-streams-in-nodejs/
- 5: https://github.com/rvagg/isstream
- 6: https://snyk.io/blog/malicious-code-found-in-npm-package-event-stream/
- 7: https://www.npmjs.com/package/isstream
- 8: https://www.geeksforgeeks.org/node-js-streams/
- 9: https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident
- 10: https://www.30secondsofcode.org/js/s/typecheck-nodejs-streams

---



</details>

**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)`

```diff
- 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);
   }
 }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>core/common-util/src/StreamUtil.ts (1)</summary><blockquote>

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

```diff
-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:

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

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

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

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

(lint/complexity/noStaticOnlyClass)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

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

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 252a0adf5525c6ccfb3a9213cb70c54a07a5c142 and 1d248178fa9e2fe47609d869df6d7e246709e865.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `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)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* plugin/tegg/package.json

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* plugin/controller/test/http/request.test.ts
* plugin/tegg/lib/ctx_lifecycle_middleware.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>core/common-util/src/StreamUtil.ts</summary>

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

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

(lint/complexity/noStaticOnlyClass)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (9)</summary>

* 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)

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>core/common-util/src/StreamUtil.ts (1)</summary>

`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.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

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

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

(lint/complexity/noStaticOnlyClass)

</details>

</details>

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-17T12:04:13Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>core/common-util/src/StreamUtil.ts (2)</summary><blockquote>

`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.

```diff
-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:

```typescript
export const StreamUtil = {
  isStream(obj: any): boolean {
    return obj instanceof Stream;
  }
};
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

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

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

(lint/complexity/noStaticOnlyClass)

</details>

</details>

---

`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)

```diff
-  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');
+  }
```

</blockquote></details>
<details>
<summary>plugin/tegg/lib/ctx_lifecycle_middleware.ts (1)</summary><blockquote>

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

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

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

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

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

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

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 1d248178fa9e2fe47609d869df6d7e246709e865 and fbdeac46a98e6d1adcc7a10f8278dfefa6d2deda.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `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)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* plugin/tegg/package.json

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* plugin/controller/test/http/request.test.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>plugin/tegg/lib/ctx_lifecycle_middleware.ts (1)</summary><blockquote>

<details>
<summary>core/common-util/src/StreamUtil.ts (1)</summary>

* `StreamUtil` (3-7)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>core/common-util/src/StreamUtil.ts</summary>

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

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

(lint/complexity/noStaticOnlyClass)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (9)</summary>

* 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)

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>plugin/tegg/lib/ctx_lifecycle_middleware.ts (1)</summary>

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

```diff
-    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();
```

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-17T12:43:11Z
user: login: killagu
body: 需要补充异常 case
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T03:13:52Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>plugin/tegg/lib/ctx_lifecycle_middleware.ts (1)</summary><blockquote>

`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.



```diff
     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);
       }
     }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>plugin/controller/test/fixtures/apps/http-inject-app/app/controller/AppController.ts (2)</summary><blockquote>

`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


```diff
     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.


```diff
     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>';
     }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

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

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4edc0fdd4e4c45f82e7cb443ca52d038fed7e4c6 and f02c8586ffa4783b1f91fb2e83393ca3dd9d89a9.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `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)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* plugin/controller/package.json

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* plugin/tegg/package.json
* plugin/controller/test/http/request.test.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>plugin/tegg/lib/ctx_lifecycle_middleware.ts (2)</summary><blockquote>

<details>
<summary>core/common-util/src/StreamUtil.ts (1)</summary>

* `StreamUtil` (3-7)

</details>
<details>
<summary>core/eventbus-runtime/src/SingletonEventBus.ts (1)</summary>

* `awaitFirst` (61-63)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* 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)

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>plugin/tegg/lib/ctx_lifecycle_middleware.ts (1)</summary>

`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.

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

`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.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-18T07:25:44Z
user: login: killagu
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T07:30:59Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>📜 Review details</summary>

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

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between f02c8586ffa4783b1f91fb2e83393ca3dd9d89a9 and c7e38d5767193aa89e148884c1f5efe55b70e78d.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `plugin/controller/test/http/request.test.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 GitHub Check: Runner-macos (18)</summary>

<details>
<summary>plugin/controller/test/http/request.test.ts</summary>

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

</details>

</details>
<details>
<summary>🪛 GitHub Check: Runner-macos (16)</summary>

<details>
<summary>plugin/controller/test/http/request.test.ts</summary>

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

</details>

</details>
<details>
<summary>🪛 GitHub Check: Runner-ubuntu (22)</summary>

<details>
<summary>plugin/controller/test/http/request.test.ts</summary>

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

</details>

</details>
<details>
<summary>🪛 GitHub Check: Runner-macos (20)</summary>

<details>
<summary>plugin/controller/test/http/request.test.ts</summary>

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

</details>

</details>
<details>
<summary>🪛 GitHub Check: Runner-ubuntu (18)</summary>

<details>
<summary>plugin/controller/test/http/request.test.ts</summary>

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

</details>

</details>
<details>
<summary>🪛 GitHub Check: Runner-ubuntu (16)</summary>

<details>
<summary>plugin/controller/test/http/request.test.ts</summary>

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

</details>

</details>
<details>
<summary>🪛 GitHub Check: Runner-ubuntu (20)</summary>

<details>
<summary>plugin/controller/test/http/request.test.ts</summary>

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

</details>

</details>
<details>
<summary>🪛 GitHub Actions: Continuous integration</summary>

<details>
<summary>plugin/controller/test/http/request.test.ts</summary>

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

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: Analyze (javascript)
* GitHub Check: Analyze (typescript)

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>plugin/controller/test/http/request.test.ts (1)</summary>

`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.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-18T07:33:18Z
user: login: killagu
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T08:28:25Z

Review comments:
diff_hunk: @@ -0,0 +1,148 @@
+import { Readable, Writable, Stream, Transform } from 'node:stream';
+import { EventEmitter } from 'node:events';
+
+export class StreamUtil {
+  /**
+   * 判断对象是否是基本的 stream
+   */
+  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) &&
+      typeof (obj as Readable).read === 'function' &&
+      typeof (obj as Readable).readable === 'boolean';
+  }
+
+  /**
+   * 判断是否是可写流
+   */
+  static isWritableStream(obj: any): boolean {
+    return this.isStream(obj) &&
+      typeof (obj as Writable).write === 'function' &&
+      typeof (obj as Writable).writable === 'boolean';
+  }
+
+  /**
+   * 判断是否是双工流
+   */
+  static isDuplexStream(obj: any): boolean {
+    return this.isStream(obj) &&
+      this.isReadableStream(obj) &&
+      this.isWritableStream(obj);
+  }
+
+  /**
+   * 判断是否是转换流
+   */
+  static isTransformStream(obj: any): boolean {
+    return this.isStream(obj) &&
+      this.isDuplexStream(obj) &&
+      typeof (obj as Transform)._transform === 'function';
+  }
+
+  /**
+   * 安全地处理 stream 错误
+   */
+  static handleStreamError(stream: Stream, errorHandler?: (error: Error) => void): void {
+    if (!this.isStream(stream)) {
+      throw new Error('Invalid stream object');
+    }
+
+    const handler = errorHandler || ((error: Error) => {
+      console.error('Stream error:', error);
+    });
+
+    stream.on('error', handler);
+  }
+
+  /**
+   * 安全地销毁 stream
+   */
+  static destroyStream(stream: Stream, error?: Error): void {
+    if (!this.isStream(stream)) {
+      throw new Error('Invalid stream object');
+    }
+
+    if (!(stream as any).destroyed) {
+      (stream as any).destroy(error);
+    }
+  }
+
+  /**
+   * 安全地结束可写流
+   */
+  static endStream(stream: Writable, chunk?: any, encoding?: BufferEncoding): Promise<void> {
+    if (!this.isWritableStream(stream)) {
+      throw new Error('Invalid writable stream');
+    }
+
+    return new Promise((resolve, reject) => {
+      if ((stream as any).destroyed) {
+        resolve();
+        return;
+      }
+
+      const handleError = (error: Error) => {
+        stream.removeListener('finish', handleFinish);
+        reject(error);
+      };
+
+      const handleFinish = () => {
+        stream.removeListener('error', handleError);
+        resolve();
+      };
+
path: core/common-util/src/StreamUtil.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix the 'handleFinish' used before definition error**

The `handleFinish` function is referenced before its definition, causing ESLint to fail. While this works at runtime, it violates best practices for code organization.


Apply this fix:

```diff
-      const handleError = (error: Error) => {
-        stream.removeListener('finish', handleFinish);
-        reject(error);
-      };
-
-      const handleFinish = () => {
-        stream.removeListener('error', handleError);
-        resolve();
-      };

+      const handleFinish = () => {
+        stream.removeListener('error', handleError);
+        resolve();
+      };
+
+      const handleError = (error: Error) => {
+        stream.removeListener('finish', handleFinish);
+        reject(error);
+      };
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
      const handleFinish = () => {
        stream.removeListener('error', handleError);
        resolve();
      };

      const handleError = (error: Error) => {
        stream.removeListener('finish', handleFinish);
        reject(error);
      };
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: Runner-macos (20)</summary>

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

</details>
<details>
<summary>🪛 GitHub Check: Runner-ubuntu (20)</summary>

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

</details>
<details>
<summary>🪛 GitHub Check: Runner-ubuntu (18)</summary>

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

</details>
<details>
<summary>🪛 GitHub Check: Runner-macos (16)</summary>

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

</details>
<details>
<summary>🪛 GitHub Check: Runner-macos (18)</summary>

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

</details>
<details>
<summary>🪛 GitHub Check: Runner-ubuntu (16)</summary>

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

</details>
<details>
<summary>🪛 GitHub Check: Runner-ubuntu (22)</summary>

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

</details>
<details>
<summary>🪛 GitHub Actions: Continuous integration</summary>

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

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 8e7710d
created_at: 2025-04-17T08:21:35Z
author_association: NONE
diff_hunk: @@ -50,6 +50,15 @@ describe('plugin/controller/test/http/request.test.ts', () => {
           assert(res.body.cookies === 'foo');
         });
     });
+
+    it('stream should work', async () => {
+      await app.httpRequest()
+        .get('/apps/stream')
+        .expect(200)
+        .expect(res => {
+          console.log(res.text);
+        });
+    });
path: plugin/controller/test/http/request.test.ts
user: login: coderabbitai[bot]
body: _🛠️ 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:

```diff
  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');
      });
  });
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    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');
        });
    });
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-17T08:21:35Z
author_association: NONE
diff_hunk: @@ -0,0 +1,148 @@
+import { Readable, Writable, Stream, Transform } from 'node:stream';
+import { EventEmitter } from 'node:events';
+
+export class StreamUtil {
+  /**
+   * 判断对象是否是基本的 stream
+   */
+  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) &&
+      typeof (obj as Readable).read === 'function' &&
+      typeof (obj as Readable).readable === 'boolean';
+  }
+
+  /**
+   * 判断是否是可写流
+   */
+  static isWritableStream(obj: any): boolean {
+    return this.isStream(obj) &&
+      typeof (obj as Writable).write === 'function' &&
+      typeof (obj as Writable).writable === 'boolean';
+  }
+
+  /**
+   * 判断是否是双工流
+   */
+  static isDuplexStream(obj: any): boolean {
+    return this.isStream(obj) &&
+      this.isReadableStream(obj) &&
+      this.isWritableStream(obj);
+  }
+
+  /**
+   * 判断是否是转换流
+   */
+  static isTransformStream(obj: any): boolean {
+    return this.isStream(obj) &&
+      this.isDuplexStream(obj) &&
+      typeof (obj as Transform)._transform === 'function';
+  }
+
+  /**
+   * 安全地处理 stream 错误
+   */
+  static handleStreamError(stream: Stream, errorHandler?: (error: Error) => void): void {
+    if (!this.isStream(stream)) {
+      throw new Error('Invalid stream object');
+    }
+
+    const handler = errorHandler || ((error: Error) => {
+      console.error('Stream error:', error);
+    });
+
+    stream.on('error', handler);
+  }
path: core/common-util/src/StreamUtil.ts
user: login: coderabbitai[bot]
body: _🛠️ 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.

```diff
-    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.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    const handler = errorHandler || ((error: Error) => {
      console.error('Stream error:', error);
    });

    stream.once('error', handler);
  }
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[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)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-17T08:25:15Z
author_association: NONE
diff_hunk: @@ -0,0 +1,35 @@
+import { Stream } from 'node:stream';
+import { EventEmitter } from 'node:events';
+
+export class StreamUtil {
+  static isStream(obj: any): boolean {
+    return obj !== null &&
+      typeof obj === 'object' &&
+      typeof obj.pipe === 'function' &&
+      typeof obj.on === 'function' &&
+      obj instanceof EventEmitter;
+  }
+
+  static untilEnd(stream: Stream, callback: () => void): void {
+    if (!this.isStream(stream)) {
+      throw new Error('Invalid stream object');
+    }
+
+    if ((stream as any).destroyed) {
+      callback();
+      return;
+    }
+
+    const handleEnd = () => {
+      console.log('handle end');
path: core/common-util/src/StreamUtil.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Remove debug console.log statement.**

The console.log statement appears to be debugging code that shouldn't be included in the production version.

```diff
-      console.log('handle end');
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion

`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 252a0ad
created_at: 2025-04-17T09:44:41Z
author_association: NONE
diff_hunk: @@ -0,0 +1,35 @@
+import { Stream } from 'node:stream';
+import { EventEmitter } from 'node:events';
+
+export class StreamUtil {
+  static isStream(obj: any): boolean {
+    return obj !== null &&
+      typeof obj === 'object' &&
+      typeof obj.pipe === 'function' &&
+      typeof obj.on === 'function' &&
+      obj instanceof EventEmitter;
+  }
+
+  static untilEnd(stream: Stream, callback: () => void): void {
+    if (!this.isStream(stream)) {
path: core/common-util/src/StreamUtil.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**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.

```diff
-    if (!this.isStream(stream)) {
+    if (!StreamUtil.isStream(stream)) {
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    if (!StreamUtil.isStream(stream)) {
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[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)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 1d24817
created_at: 2025-04-17T09:44:42Z
author_association: NONE
diff_hunk: @@ -0,0 +1,35 @@
+import { Stream } from 'node:stream';
+import { EventEmitter } from 'node:events';
+
+export class StreamUtil {
+  static isStream(obj: any): boolean {
path: core/common-util/src/StreamUtil.ts
user: login: killagu
body: is.stream
created_at: 2025-04-17T11:42:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,29 @@
+import { Stream } from 'node:stream';
+
+export class StreamUtil {
+  static 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');
+    }
+
+    if ((stream as any).destroyed) {
+      callback();
+      return;
+    }
+
+    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);
path: core/common-util/src/StreamUtil.ts
user: login: coderabbitai[bot]
body: _🛠️ 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.

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


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    stream.once('end', handleEnd);
    stream.once('error', handleEnd);
    stream.once('close', handleEnd);
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-17T11:46:53Z
author_association: NONE
diff_hunk: @@ -22,14 +24,20 @@ export default async function ctxLifecycleMiddleware(ctx: TEggPluginContext, nex
   if (teggCtx.init) {
     await teggCtx.init(lifecycle);
   }
+
+  function doDestory() {
+    teggCtx.destroy(lifecycle).catch(e => {
+      e.message = '[tegg/ctxLifecycleMiddleware] destroy tegg ctx failed:' + e.message;
+      ctx.logger.error(e);
+    });
+  }
   try {
     await next();
   } finally {
-    if (teggCtx.destroy) {
-      teggCtx.destroy(lifecycle).catch(e => {
-        e.message = '[tegg/ctxLifecycleMiddleware] destroy tegg ctx failed:' + e.message;
-        ctx.logger.error(e);
-      });
+    if (StreamUtil.isStream(ctx.response.body)) {
+      awaitEvent(ctx.response.body, 'close').then(doDestory);
+    } else {
path: plugin/tegg/lib/ctx_lifecycle_middleware.ts
user: login: coderabbitai[bot]
body: _🛠️ 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.

```diff
-    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);
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    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 {
`````

</details>

<!-- suggestion_end -->

---

_🛠️ 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.

```diff
-    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
+        });
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    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 {
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-17T12:43:10Z
author_association: NONE
diff_hunk: @@ -22,14 +24,27 @@ export default async function ctxLifecycleMiddleware(ctx: TEggPluginContext, nex
   if (teggCtx.init) {
     await teggCtx.init(lifecycle);
   }
+
+  async function doDestory() {
+    if (StreamUtil.isStream(ctx.response.body)) {
+      try {
+        await awaitFirst(ctx.response.body, [ 'close', 'error' ]);
+      } catch (error) {
+        ctx.res.destroy(error);
+      }
+    }
+    try {
+      if (!teggCtx.destroy) {
+        await teggCtx.destroy(lifecycle);
+      }
+    } catch (e) {
+      e.message = '[tegg/ctxLifecycleMiddleware] destroy tegg ctx failed:' + e.message;
+      ctx.logger.error(e);
+    }
+  }
   try {
     await next();
   } finally {
-    if (teggCtx.destroy) {
-      teggCtx.destroy(lifecycle).catch(e => {
-        e.message = '[tegg/ctxLifecycleMiddleware] destroy tegg ctx failed:' + e.message;
-        ctx.logger.error(e);
-      });
-    }
+    doDestory();
path: plugin/tegg/lib/ctx_lifecycle_middleware.ts
user: login: coderabbitai[bot]
body: _🛠️ 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.


```diff
-    doDestory();
+    doDestroy();
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    doDestroy();
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-18T07:25:43Z
author_association: NONE
diff_hunk: @@ -22,14 +24,27 @@
   if (teggCtx.init) {
     await teggCtx.init(lifecycle);
   }
+
+  async function doDestory() {
+    if (StreamUtil.isStream(ctx.response.body)) {
+      try {
+        await awaitFirst(ctx.response.body, [ 'close', 'error' ]);
+      } catch (error) {
+        ctx.res.destroy(error);
+      }
+    }
+    try {
+      if (!teggCtx.destroy) {
+        await teggCtx.destroy(lifecycle);
+      }
+    } catch (e) {
+      e.message = '[tegg/ctxLifecycleMiddleware] destroy tegg ctx failed:' + e.message;
+      ctx.logger.error(e);
+    }
+  }
path: plugin/tegg/lib/ctx_lifecycle_middleware.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix typo in function name and logical error in condition**

There are two issues in this function:

1. There's a typo in the function name: `doDestory` should be `doDestroy`.
2. The conditional logic at line 37 is inverted - it should check `if (teggCtx.destroy)` rather than `if (!teggCtx.destroy)`.


```diff
-  async function doDestory() {
+  async function doDestroy() {
     if (StreamUtil.isStream(ctx.response.body)) {
       try {
         await awaitFirst(ctx.response.body, [ 'close', 'error' ]);
       } catch (error) {
         ctx.res.destroy(error);
       }
     }
     try {
-      if (!teggCtx.destroy) {
+      if (teggCtx.destroy) {
         await teggCtx.destroy(lifecycle);
       }
     } catch (e) {
       e.message = '[tegg/ctxLifecycleMiddleware] destroy tegg ctx failed:' + e.message;
       ctx.logger.error(e);
     }
   }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
  async function doDestroy() {
    if (StreamUtil.isStream(ctx.response.body)) {
      try {
        await awaitFirst(ctx.response.body, [ 'close', 'error' ]);
      } catch (error) {
        ctx.res.destroy(error);
      }
    }
    try {
      if (teggCtx.destroy) {
        await teggCtx.destroy(lifecycle);
      }
    } catch (e) {
      e.message = '[tegg/ctxLifecycleMiddleware] destroy tegg ctx failed:' + e.message;
      ctx.logger.error(e);
    }
  }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit ec7ab6a
created_at: 2025-04-18T07:25:43Z
author_association: NONE
diff_hunk: @@ -50,6 +50,23 @@
           assert(res.body.cookies === 'foo');
         });
     });
+
+    it('stream should work', async () => {
+      await app.httpRequest()
+        .get('/apps/stream')
+        .expect(200)
+        .expect(res => {
+          assert(res.text.includes('流式内容5'));
+        });
+    });
+
+    it('error stream should work', async () => {
+      await assert.rejects(
+        app.httpRequest()
+          .get('/apps/error_stream')
+          .expect(200)
+      , /Error: aborted/);
+    });
path: plugin/controller/test/http/request.test.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix indentation in the assert.rejects block**

The indentation on line 68 is causing pipeline failures. The static analysis tools expect 8 spaces of indentation rather than 6.


```diff
  it('error stream should work', async () => {
    await assert.rejects(
      app.httpRequest()
        .get('/apps/error_stream')
        .expect(200)
-    , /Error: aborted/);
+      , /Error: aborted/);
  });
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    it('error stream should work', async () => {
      await assert.rejects(
        app.httpRequest()
          .get('/apps/error_stream')
          .expect(200)
        , /Error: aborted/);
    });
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: Runner-macos (18)</summary>

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

</details>
<details>
<summary>🪛 GitHub Check: Runner-macos (16)</summary>

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

</details>
<details>
<summary>🪛 GitHub Check: Runner-ubuntu (22)</summary>

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

</details>
<details>
<summary>🪛 GitHub Check: Runner-macos (20)</summary>

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

</details>
<details>
<summary>🪛 GitHub Check: Runner-ubuntu (18)</summary>

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

</details>
<details>
<summary>🪛 GitHub Check: Runner-ubuntu (16)</summary>

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

</details>
<details>
<summary>🪛 GitHub Check: Runner-ubuntu (20)</summary>

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

</details>
<details>
<summary>🪛 GitHub Actions: Continuous integration</summary>

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

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 531132d
created_at: 2025-04-18T07:33:17Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-17T08:18:41Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

"""
## 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)

```mermaid
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
```

## Possibly related issues

- eggjs/tegg#304: This PR implements the `StreamUtil` class and updates middleware to defer context destruction until stream completion, directly addressing the issue's objective to prevent premature destruction during streaming responses.

## 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!  
> 🌊
"""

<!-- walkthrough_end -->
<!-- This is an auto-generated comment: all tool run failures by coderabbit.ai -->

> [!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.
> 
> <details>
> <summary>🔧 ESLint</summary>
> 
> > 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.
> 
> <details>
> <summary>plugin/tegg/lib/ctx_lifecycle_middleware.ts</summary>
> 
> 
> 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.
> 
> 
> </details>
> 
> </details>

<!-- end of auto-generated comment: all tool run failures by coderabbit.ai -->


---

<details>
<summary>📜 Recent review details</summary>

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

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 531132d8d1dbac91e26eec443e1e69a7b01f7ee4 and ec7ab6a9174e2fd4c4187045a215e6b83dc410bd.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `plugin/tegg/lib/ctx_lifecycle_middleware.ts` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* plugin/tegg/lib/ctx_lifecycle_middleware.ts

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (9)</summary>

* 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)

</details>

</details>
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=eggjs/tegg&utm_content=302):

- 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](https://docs.coderabbit.ai/finishing-touches/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](https://docs.coderabbit.ai/guides/configure-coderabbit) 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](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: fengmk2
created_at: 2025-04-17T11:23:37Z
body: @coderabbitai 建一个 issue 跟进此 pr 会 pick 到 next 分支
author_association: MEMBER
user: login: coderabbitai[bot]
created_at: 2025-04-17T11:24:25Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
> [!TIP]
> For best results, initiate chat on the files or code changes.

<details>
<summary>🧩 Analysis chain</summary>

🏁 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

---



</details>

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

Issue 已成功创建：https://github.com/eggjs/tegg/issues/304

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

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


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer><details>
<summary>🧩 Analysis chain</summary>

🏁 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

---



</details>

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

Issue 已成功创建：https://github.com/eggjs/tegg/issues/304

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

(\_/)
(•ᴥ•)
/ >📝 完成!</answer></rawResChunk> -->
author_association: NONE