Skip to content

Add regression test for write-after-endInput race in ProcessTransport#229

Open
dumko2001 wants to merge 1 commit into
anthropics:mainfrom
dumko2001:fix/prevent-write-after-endinput
Open

Add regression test for write-after-endInput race in ProcessTransport#229
dumko2001 wants to merge 1 commit into
anthropics:mainfrom
dumko2001:fix/prevent-write-after-endinput

Conversation

@dumko2001

Copy link
Copy Markdown

Summary

Regression test for #148. Reproduces the race where `handleControlRequest()` is launched fire-and-forget in `readMessages()`. When a `result` message arrives immediately after a `control_request`, `endInput()` closes stdin before the async handler finishes (e.g. `canUseTool` delay). The handler then calls `write()` on the ended stream — throwing `ERR_STREAM_WRITE_AFTER_END`. The `catch` block attempts a second `write()`, which becomes an unhandled rejection and crashes Node.js.

The test currently fails against the published SDK, confirming the bug. It will pass once the fix is applied.

Fix (for SDK source)

// ProcessTransport class:
private stdinEnded = false;

endInput(): void {
  this.stdinEnded = true; // set before .end() so concurrent writes see it immediately
  if (this.processStdin) this.processStdin.end();
}

write(data: string): void {
  if (this.stdinEnded) return; // first guard, silent drop, never throws
  // ... rest unchanged
}

Silent drop is safe — after endInput() the subprocess has already emitted its result and will not read further stdin.

Changes

  • tests/process-transport-write-after-endinput.test.ts — regression test using spawnClaudeCodeProcess to inject a controlled mock process, with a canUseTool delay that guarantees the race fires
  • package.json, tsconfig.json, vitest.config.ts, .gitignore — minimum Vitest infrastructure (not present on main)

Closes #148

Reproduces the race described in anthropics#148: handleControlRequest() is launched
fire-and-forget in readMessages(). When a result message arrives immediately
after a control_request, endInput() closes stdin before the handler finishes
its async work (e.g. canUseTool). The handler then calls write() on the ended
stream. The catch block attempts a second write(), which becomes an unhandled
rejection and crashes Node.js.

The test fails against the current SDK (confirming the bug) and will pass
once the fix is applied in ProcessTransport:
  private stdinEnded = false;
  endInput(): void { this.stdinEnded = true; if (this.processStdin) this.processStdin.end(); }
  write(data: string): void { if (this.stdinEnded) return; /* ... */ }

Also includes the minimum test infrastructure (package.json, tsconfig.json,
vitest.config.ts, .gitignore) needed to run the Vitest suite.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Race condition: endInput() allows writes to ended stream causing 'write after end' errors

1 participant