Skip to content

fix(envd): distinguish expected stdin lifecycle errors from CodeInternal - #3628

Open
AdaAibaby wants to merge 1 commit into
e2b-dev:mainfrom
AdaAibaby:fix/envd-stdin-lifecycle-error-codes
Open

fix(envd): distinguish expected stdin lifecycle errors from CodeInternal#3628
AdaAibaby wants to merge 1 commit into
e2b-dev:mainfrom
AdaAibaby:fix/envd-stdin-lifecycle-error-codes

Conversation

@AdaAibaby

@AdaAibaby AdaAibaby commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Problem

Process.SendInput maps every Handler.WriteStdin / WriteTty failure to CodeInternal, and CloseStdin maps every failure to CodeUnknown. Several of these are expected process-lifecycle outcomes rather than envd faults:

  • stdin was started disabled, or has already been closed;
  • a PTY-backed process receives stdin (input belongs on the pty);
  • the process exits and its stdin read end is gone, so the write end sees EPIPE / os.ErrClosed (`file already closed`) / io.ErrClosedPipe.

Because CodeInternal is reserved for serious invariant failures, clients cannot distinguish a normal process-state transition from an unexpected envd failure. It also races the Start response stream: SendInput can return CodeInternal before the process's EndEvent arrives on the independent stream.

Fixes #3622.

Fix

Introduce typed sentinels (ErrStdinUnavailable, ErrStdinOnPty, ErrTtyUnavailable, ErrCloseStdinOnPty) and an InputErrorCode mapper in the handler package, mirroring the existing StartErrorCode pattern. The service layer (input.go) now maps input failures through it:

Condition Code
process selector no longer exists CodeNotFound (already handled by getProcess)
process exists but cannot accept stdin (disabled/closed, wrong pipe for type, exited) CodeFailedPrecondition
unexpected underlying I/O failure CodeInternal

CloseStdin follows the same taxonomy (previously CodeUnknown).

The EndEvent remains the authoritative exit signal; a SendInput error alone is not treated as the terminal process result.

Note: after a process exits, cmd.Wait() closes the stdin write end, so the observed error is often Go's `file already closed` (os.ErrClosed) rather than raw EPIPE — the mapper matches both, plus io.ErrClosedPipe / fs.ErrClosed.

Compatibility

This changes observable Connect error codes. Callers that only check for a non-nil error are unaffected. Python/JS SDK behavior should be checked before merging, as flagged in the issue.

Tests

Added input_error_test.go:

  • table-driven InputErrorCode mapping (sentinels, wrapped sentinels, EPIPE/closed-pipe variants, EIO/opaque -> internal);
  • client-observed connect.CodeOf assertions;
  • WriteStdin returns the typed sentinels for the disabled / pty-rejection preconditions;
  • a reproduction of envd: distinguish expected stdin lifecycle errors from CodeInternal #3622: a short-lived non-PTY process whose stdin read end is gone after exit yields CodeFailedPrecondition, not CodeInternal.

go build ./..., go vet, gofmt -l, and the full ./internal/services/process/... suite all pass.

Process.SendInput and CloseStdin previously mapped every WriteStdin /
WriteTty / CloseStdin failure to CodeInternal (CloseStdin to CodeUnknown).
Several of these are expected process-lifecycle outcomes, not envd faults:

  - stdin was started disabled, or has already been closed;
  - a PTY-backed process received stdin (input belongs on the pty);
  - the process exited and its stdin read end is gone, so the write end
    sees EPIPE / os.ErrClosed ("file already closed") / io.ErrClosedPipe.

Collapsing these into CodeInternal means clients cannot tell a normal
process-state transition from a genuine invariant failure, and it races
the Start response stream: SendInput can return CodeInternal before the
process's EndEvent arrives on the independent stream.

Introduce typed sentinels (ErrStdinUnavailable, ErrStdinOnPty,
ErrTtyUnavailable, ErrCloseStdinOnPty) and an InputErrorCode mapper in the
handler package, mirroring StartErrorCode:

  - process selector missing            -> CodeNotFound   (already handled)
  - process cannot accept stdin/pty     -> CodeFailedPrecondition
  - unexpected underlying I/O failure   -> CodeInternal

CloseStdin follows the same taxonomy. The EndEvent remains the
authoritative exit signal; an input error alone is not the terminal
process result.

Callers that only check for a non-nil error are unaffected.

Fixes e2b-dev#3622
@AdaAibaby
AdaAibaby force-pushed the fix/envd-stdin-lifecycle-error-codes branch from cc2f714 to 425e8ad Compare September 8, 2026 05:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

envd: distinguish expected stdin lifecycle errors from CodeInternal

2 participants