-
Notifications
You must be signed in to change notification settings - Fork 1.8k
fix: #780 onerror and other listener not remove after client close #1322
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@modelcontextprotocol/client': patch | ||
| --- | ||
|
|
||
| Detach stdio transport stdout/stdin listeners on `close()`. Previously, if the child process wrote to stdout during shutdown (after `close()` was called), the still-attached data listener would attempt to parse it as JSON-RPC and emit a spurious parse error via `onerror`. Fixes #780. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -95,6 +95,8 @@ export class StdioClientTransport implements Transport { | |
| private _readBuffer: ReadBuffer = new ReadBuffer(); | ||
| private _serverParams: StdioServerParameters; | ||
| private _stderrStream: PassThrough | null = null; | ||
| private _onServerDataHandler?: (chunk: Buffer) => void; | ||
| private _onServerErrorHandler?: (error: Error) => void; | ||
|
|
||
| onclose?: () => void; | ||
| onerror?: (error: Error) => void; | ||
|
|
@@ -130,33 +132,31 @@ export class StdioClientTransport implements Transport { | |
| cwd: this._serverParams.cwd | ||
| }); | ||
|
|
||
| this._onServerDataHandler = (chunk: Buffer) => { | ||
| this._readBuffer.append(chunk); | ||
| this.processReadBuffer(); | ||
| }; | ||
| this._onServerErrorHandler = (error: Error) => { | ||
| this.onerror?.(error); | ||
| }; | ||
|
|
||
| this._process.stdout?.on('data', this._onServerDataHandler); | ||
| this._process.stdout?.on('error', this._onServerErrorHandler); | ||
| this._process.stdin?.on('error', this._onServerErrorHandler); | ||
|
|
||
| this._process.on('error', error => { | ||
| reject(error); | ||
| this.onerror?.(error); | ||
| }); | ||
|
|
||
| this._process.on('spawn', () => { | ||
| resolve(); | ||
| }); | ||
|
|
||
| this._process.on('close', _code => { | ||
| this._process.once('spawn', () => resolve()); | ||
| this._process.once('close', _code => { | ||
| if (this._process) { | ||
| this.cleanupListeners(this._process); | ||
| } | ||
| this._process = undefined; | ||
| this.onclose?.(); | ||
| }); | ||
|
Comment on lines
+154
to
160
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 The Extended reasoning...What the bug is and how it manifestsThis PR introduces The specific code path that triggers itIn Why existing code does not prevent itThe What the impact isAny caller that does How to fix itCapture let startReject: ((e: Error) => void) | undefined;
return new Promise((resolve, reject) => {
startReject = reject;
// ... spawn ...
this._onProcessErrorHandler = error => {
startReject?.(error); // call reject even if handler is detached
this.onerror?.(error);
};
// In once("close"):
this._process.once("close", _code => {
startReject = undefined; // settled
...
});
});Alternatively, track a Step-by-step proof
|
||
|
|
||
| this._process.stdin?.on('error', error => { | ||
| this.onerror?.(error); | ||
| }); | ||
|
|
||
| this._process.stdout?.on('data', chunk => { | ||
| this._readBuffer.append(chunk); | ||
| this.processReadBuffer(); | ||
| }); | ||
|
|
||
| this._process.stdout?.on('error', error => { | ||
| this.onerror?.(error); | ||
| }); | ||
|
|
||
| if (this._stderrStream && this._process.stderr) { | ||
| this._process.stderr.pipe(this._stderrStream); | ||
| } | ||
|
|
@@ -202,8 +202,19 @@ export class StdioClientTransport implements Transport { | |
| } | ||
| } | ||
|
|
||
| private cleanupListeners(process: ChildProcess) { | ||
| if (this._onServerDataHandler) { | ||
| process.stdout?.off('data', this._onServerDataHandler); | ||
| } | ||
| if (this._onServerErrorHandler) { | ||
| process.stdout?.off('error', this._onServerErrorHandler); | ||
| process.stdin?.off('error', this._onServerErrorHandler); | ||
| } | ||
| } | ||
|
Comment on lines
+207
to
+218
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 The Extended reasoning...What the bug isIn The specific code pathInside Why existing code doesn't prevent itThe current code is functionally correct: every use of ImpactThis is a maintenance trap rather than an active bug. Any developer adding code inside Step-by-step proof of the shadowing
FixRename the parameter to private cleanupListeners(childProcess: ChildProcess) {
if (this._onServerDataHandler) {
childProcess.stdout?.off('data', this._onServerDataHandler);
}
// ...
if (this._onProcessErrorHandler) {
childProcess.off('error', this._onProcessErrorHandler);
}
}This is a one-line rename with zero behavioral impact. |
||
|
|
||
| async close(): Promise<void> { | ||
| if (this._process) { | ||
| this.cleanupListeners(this._process); | ||
| const processToClose = this._process; | ||
| this._process = undefined; | ||
|
|
||
|
Comment on lines
220
to
225
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 In Extended reasoning...What the bug is and how it manifestsIn If the child process has already exited (its read end of the pipe is closed), calling The specific code path that triggers itBefore the PR, the anonymous stdin error listener was registered and never removed, so EPIPE was routed (spuriously) to Why existing safeguards do not prevent itThe ImpactThis is a regression introduced by the PR. Before the PR, EPIPE on stdin was silently routed to Step-by-step proof
How to fix itReorder: call |
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.