fix(types): ExecProcess streams are undefined, not null, when absent by PedroRosalba · Pull Request #7847 · cloudflare/workerd · GitHub
Skip to content

fix(types): ExecProcess streams are undefined, not null, when absent - #7847

Open
PedroRosalba wants to merge 1 commit into
cloudflare:mainfrom
PedroRosalba:fix/container-execprocess-streams
Open

PedroRosalba wants to merge 1 commit into
cloudflare:mainfrom
PedroRosalba:fix/container-execprocess-streams

Conversation

@PedroRosalba

Copy link
Copy Markdown

Problem

ExecProcess (returned by ctx.container.exec()) declares readonly stdin: WritableStream | null, readonly stdout: ReadableStream | null, readonly stderr: ReadableStream | null. The getters return jsg::Optional<...>, and JSG wraps an empty jsg::Optional as undefined; only kj::Maybe becomes null. Absent is the common case: stdin unless stdin: "pipe" (or a PTY), stdout with stdout: "ignore", stderr with stderr: "ignore" or "combined" (so also with a PTY). As a result proc.stdin === null is never true, and if (proc.stdout !== null) proc.stdout.getReader() compiles and throws TypeError: Cannot read properties of undefined.

The Containers docs already say: "A stream that the process does not provide is undefined." and "The Workers TypeScript types declare stdin, stdout, and stderr as | null. Test for a stream, such as if (process.stderr), instead of comparing with null."

Fix

Declare the three as optional (readonly stdin?: WritableStream, ...) in the JSG_TS_OVERRIDE in src/workerd/api/container.h, which is what the generator emits for jsg::Optional properties elsewhere (DurableObjectState.container?, ContainerSnapshot.name?) and what Request.cf does for a jsg::Optional getter in an override. Types only.

Type-level breaking change: under strictNullChecks, === null checks on these fields become a compile error, which is the point: they were never true.

Tests

  • bazel test //types:test/types/container (new; fails on the previous snapshot)
  • //src/workerd/server/tests/container-client:container-client@ (existing runtime coverage; needs Docker)
  • Verified on the built binary against a Docker container in the default, stdout: "ignore" + stderr: "ignore", stderr: "combined", stdin: "pipe" and pty: true configurations: every absent stream is typeof "undefined" and === null is false.
  • just generate-types reproduces the snapshot edits byte for byte.

Tested on macOS (Apple Silicon) only.

`ExecProcess.stdin`, `.stdout` and `.stderr` are `jsg::Optional` getters
(`ExecProcess::getStdin/getStdout/getStderr`), and JSG wraps an empty
`jsg::Optional` as `undefined`; only `kj::Maybe` becomes `null`. The
hand-written `JSG_TS_OVERRIDE` declared all three as `X | null`, so
`proc.stdout === null` never holds, and `if (proc.stderr !== null)
proc.stderr.getReader()` compiles and throws when the stream was not
provided. Absent is the common case: stdin unless `stdin: "pipe"` (or a
PTY), stdout with `stdout: "ignore"`, stderr with `stderr: "ignore"` or
`"combined"` (so also with a PTY). The Containers docs already describe
the three fields as `| undefined`.

Declare them optional (`readonly stdin?: WritableStream`, ...), which is
what the generator emits for `jsg::Optional` properties elsewhere
(`DurableObjectState.container?`, `ContainerSnapshot.name?`) and what the
`DurableObjectStub.name?` override already does for a `jsg::Optional`
getter. Types only; no runtime change.

Verified on the built binary against a Docker container: in the default,
`stdout: "ignore"` + `stderr: "ignore"`, `stderr: "combined"`,
`stdin: "pipe"` and `pty: true` configurations every absent stream reads
`typeof undefined` and `=== null` is false.

Tests: //types:test/types/container (new; fails against the previous
snapshot), //src/workerd/server/tests/container-client:container-client@
(existing runtime coverage; needs Docker).

Precedent for an optional prototype getter in an override: Request.cf
(jsg::Optional getCf, overridden as `readonly cf?: Cf` in http.h).
@PedroRosalba
PedroRosalba requested review from a team as code owners October 20, 2026 10:52
@github-actions

Copy link
Copy Markdown

@clanky-the-clanker

Copy link
Copy Markdown

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.

1 participant