TypeScript SDK: unvalidated JSON.parse cast crashes with an opaque TypeError on two event shapes
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 125k
- Forks
- 19.4k
- PR merge metrics
- PR metrics pending
Description
What issue are you seeing?
In sdk/typescript/src/thread.ts, runStreamedInternal parses each subprocess line and casts it unchecked (lines 99-109):
let parsed: ThreadEvent;
try {
parsed = JSON.parse(item) as ThreadEvent;
} catch (error) {
throw new Error(`Failed to parse item: ${item}`, { cause: error });
}
if (parsed.type === "thread.started") {
this._id = parsed.thread_id;
} else if (parsed.type === "turn.completed") {
parsed.usage.cache_write_input_tokens ??= 0;
}
JSON.parse returns any, so the as ThreadEvent cast is a compile-time claim about runtime data coming from a subprocess. TypeScript cannot enforce it. Two well-formed JSON lines get past the try/catch and then crash on the next statement:
| Input line | Crash |
|---|---|
null |
TypeError: Cannot read properties of null (reading 'type') |
{"type":"turn.completed"} |
TypeError: Cannot read properties of undefined (reading 'cache_write_input_tokens') |
{"type":"turn.completed","usage":null} |
TypeError: Cannot read properties of null (reading 'cache_write_input_tokens') |
Because these throw from inside an async generator, the SDK user sees a bare TypeError rather than the Failed to parse item: ... error the surrounding code is clearly designed to produce for malformed output.
What steps can reproduce the bug?
Any CodexExec that yields one of the lines above. Minimal reproduction of the exact statement:
const parsed = JSON.parse('{"type":"turn.completed"}');
if (parsed.type === "thread.started") {}
else if (parsed.type === "turn.completed") {
parsed.usage.cache_write_input_tokens ??= 0; // TypeError
}
Why this looks unintended rather than an accepted invariant
- The surrounding code is defensive everywhere else:
JSON.parseis wrapped in try/catch, and thread options use optional chaining throughout (options?.model, etc.). These two dereferences are the outliers. run()already types its returnedusageasUsage | null(line 122), so a missing usage is representable downstream — it is just unreachable past this dereference.events.tsdeclaresusage: Usageas required, but that constrains the declared type, not the bytes arriving from the subprocess, e.g. from a version-skewed or truncated CLI stream.
Expected behavior
A malformed or unexpected event line should surface the existing Failed to parse item: ... error, not a bare TypeError from a property access.
Possible fix
Reject non-object payloads with the existing error and guard the usage default:
if (parsed === null || typeof parsed !== "object") {
throw new Error(`Failed to parse item: ${item}`);
}
if (parsed.type === "thread.started") {
this._id = parsed.thread_id;
} else if (parsed.type === "turn.completed" && parsed.usage) {
parsed.usage.cache_write_input_tokens ??= 0;
}
A branch with the fix and 4 regression tests is at https://github.com/Shivansh1205/codex/tree/fix/sdk-thread-event-validation — the tests fail before the change and pass after, and they confirm cache_write_input_tokens still defaults to 0 when usage is present and an existing value is preserved.
Note: tests/run.test.ts and tests/runStreamed.test.ts fail at import time in my environment on an unrelated import.meta.url / createRequire issue in exec.ts. I verified they fail identically on a clean checkout of main, so that is pre-existing and unrelated to this report.
Environment
Observed on main at commit 36f0dbe796, Node v24.14.0.
🤖 Generated with Claude Code
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in sdk/typescript/src/thread.ts, at runStreamedInternal around lines 99-109, and review the event parsing path and its existing tests. Run tests/run.test.ts and tests/runStreamed.test.ts where possible; done means malformed event lines produce the existing Failed to parse item error while valid turn.completed usage still defaults cache_write_input_tokens to 0 and preserves existing values.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- api
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 84/100