Skip to content

🐛 Handle EPIPE on stdin in POSIX process implementation#198

Open
taras wants to merge 5 commits intomainfrom
fix/process-posix-epipe
Open

🐛 Handle EPIPE on stdin in POSIX process implementation#198
taras wants to merge 5 commits intomainfrom
fix/process-posix-epipe

Conversation

@taras
Copy link
Copy Markdown
Member

@taras taras commented Mar 22, 2026

Summary

  • Add childProcess.stdin error handler in POSIX createPosixProcess to suppress EPIPE errors when writing to stdin after child exit
  • Non-EPIPE stdin errors are routed through processResult.resolve(Err(err)) — the same error propagation path used by trapError
  • Add regression test that explicitly writes to stdin after child exit to verify no uncaught exception
  • Bump @effectionx/process to 0.7.4

Root cause

The Windows implementation (win32.ts:130-137) already suppressed EPIPE on childProcess.stdin, but the POSIX implementation had no error handler at all. Any EPIPE from a write-after-exit became an uncaught exception.

Behavioral contract after fix

  • EPIPE on stdin (write-after-exit during teardown or normal lifecycle race): silently ignored
  • Non-EPIPE stdin errors: surfaced as operation errors via join()/expect()
  • stdin.send() remains fire-and-forget (void return)

Test plan

  • New test: stdin EPIPE handling > does not crash when writing to stdin after child exits
  • New fixture: read-one-line.js — reads one line from stdin, confirms via stdout, exits
  • Test explicitly exercises post-exit stdin.send() call
  • All existing tests continue to pass (3 pre-existing failures on main are unrelated — stderr matching with Node's ExperimentalWarning)

Summary by CodeRabbit

  • Chores

    • Bumped package version to 0.7.4.
  • Bug Fixes

    • Improved handling of stdin errors when writing to already-exited child processes to prevent unexpected crashes and improve stability.
  • Tests

    • Added tests covering stdin-after-exit scenarios to ensure robustness of the fix.

The POSIX createPosixProcess had no error handler on childProcess.stdin.
When a child exits before a parent stdin write completes, Node emits EPIPE
which surfaced as an uncaught exception. The Windows implementation already
handled this. This adds the same suppression for POSIX and routes non-EPIPE
stdin errors through the existing processResult error path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 22, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Updated package version and added deterministic stdin "error" handlers on POSIX and Windows paths that suppress EPIPE (logging a warning) and surface other stdin errors by resolving the process result. Added a test and fixture that verify writing to stdin after child exit is handled without crashing.

Changes

Cohort / File(s) Summary
Version Bump
process/package.json
Bumped package version from 0.7.3 to 0.7.4.
Stdin Error Handling (POSIX)
process/src/exec/posix.ts
Added a named stdinErrorHandler that suppresses EPIPE (logs a warning) and resolves non-EPIPE errors with Err(err); handler is removed in finally via .off().
Stdin Error Handling (Windows)
process/src/exec/win32.ts
Refactored inline stdin error callback into stdinErrorHandler; suppresses EPIPE (logs a warning) and resolves non-EPIPE errors with Err(err); handler is removed in finally via .off().
Tests & Fixtures
process/test/exec.test.ts, process/test/fixtures/read-one-line.js
Added stdin EPIPE handling test that writes to stdin after child exit and confirms no uncaught failure; added read-one-line.js fixture that reads one line then exits with code 0.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • cowboyd

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Policy Compliance ❌ Error The process/package.json keywords array contains non-approved keywords (effection, effectionx, spawn, exec, child-process) violating the strict Package.json Metadata Policy. Remove non-approved keywords from process/package.json and use only approved categories: testing, io, process, streams, concurrency, reactivity, interop, platform.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: handling EPIPE errors on stdin in the POSIX process implementation.
Description check ✅ Passed The pull request description comprehensively covers motivation, approach, root cause analysis, behavioral contract, and test plan aligned with the template sections.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/process-posix-epipe

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 22, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@effectionx/process@198

commit: 4c05cca

Route non-EPIPE stdin errors through processResult instead of throwing
from the event callback, matching the pattern applied in posix.ts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@taras taras requested review from cowboyd, jbolda and joshamaju March 22, 2026 15:46
Co-locate bind/unbind for the stdin error handler using Effection's
action() primitive. The cleanup function runs automatically when the
spawned task is halted during scope teardown.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@process/src/exec/stdin.ts`:
- Line 5: Duplicate tuple alias ProcessResultValue is defined in multiple exec
modules; extract it into a single shared module (e.g., create an api module) and
export it for reuse. Create/process/src/exec/api.ts with an exported type
ProcessResultValue = [number?, string?]; then remove the local alias from
stdin.ts, posix.ts, and win32.ts and replace with an import of
ProcessResultValue from that shared api module (or re-export from an index if
preferred) so all exec implementations reference the same type definition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0cafae46-d53b-409c-b673-a29e8fb4b93d

📥 Commits

Reviewing files that changed from the base of the PR and between 4468d7d and fbbcee5.

📒 Files selected for processing (3)
  • process/src/exec/posix.ts
  • process/src/exec/stdin.ts
  • process/src/exec/win32.ts

The action() approach removed the EPIPE handler too early — before
the finally block writes to stdin during Windows graceful shutdown.
Use named handler with explicit .off() at the end of finally instead,
ensuring the handler survives through all cleanup writes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@joshamaju
Copy link
Copy Markdown
Collaborator

What if we also show a warning in dev mode?

Logs a warning when writing to stdin of an already-exited child process,
helping developers spot logic bugs where writes aren't gated on join().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jbolda
Copy link
Copy Markdown
Member

jbolda commented Mar 25, 2026

Mentioned in discord but for posterity:

Is there a case where we wouldn't want to no-op outside of shutdown? I suspect it is pretty safe to ignore in shutdown. I can't think of a truly useful situation, but gut says that it might be worth somehow allowing the user to deal with the errors outside of shutdown. A possible example may be that a process that starts, looks for user input, then runs "read only" to completion?

@cowboyd
Copy link
Copy Markdown
Member

cowboyd commented Mar 25, 2026

Why are we trying to write to stdin after it has been closed? In other words, shouldn't we be detecting stdin close when it happens?

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.

4 participants