Skip to content

fix(sse): treat a data frame that parses to a non-record as malformed (#1219) - #1240

Merged
lidge-jun merged 1 commit into
lidge-jun:devfrom
snowyukitty:fix/adapters-null-sse-frame
Aug 8, 2026
Merged

fix(sse): treat a data frame that parses to a non-record as malformed (#1219)#1240
lidge-jun merged 1 commit into
lidge-jun:devfrom
snowyukitty:fix/adapters-null-sse-frame

Conversation

@snowyukitty

@snowyukitty snowyukitty commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Closes #1219

Problem

JSON.parse returns a value, not necessarily an object. JSON.parse("null") returns null without throwing, so a try/catch wrapped around the parse cannot see it. Four SSE frame parsers cast the result straight to Record<string, unknown> and dereference it on the next line, so a syntactically valid data: null frame walks past the malformed-frame guard and crashes the parser mid-stream.

You observed it through the web-search loop on adapter: openai-chat, where upstream instability makes odd frames frequent, but nothing about it is provider-specific or web-search-specific — it reaches any streamed request.

File Unguarded parse Crashing access Existing guard it escapes
src/adapters/openai-chat.ts :961 chunk.error :970 yield malformed frame + terminate
src/adapters/google.ts :502 chunk.error :510 yield malformed frame + terminate
src/adapters/anthropic.ts :989 data.type :995 debugDroppedFrame + continue
src/web-search/parse.ts :157 data.type :161 console.warn + return

The fourth row is not in the report. The issue names three adapters. src/web-search/parse.ts has the identical defect, and it sits directly in the web-search path you were actually running — parseSidecarSSE wraps its read loop in try/finally with no catch, so the TypeError escapes the function. Fixing only the three reported sites would have left your own code path broken.

Two failure modes, not one. Property access on a non-null primitive is legal in JS, so only null actually throws:

data: payload typeof Behaviour before this PR
null object throwsnull is not an object
42 / "text" / true number / string / boolean no throw; silently accepted as an empty frame
[] object (array) no throw; silently accepted as an empty frame

The crash is the reported bug. The silent acceptance is the same defect one step milder: a malformed frame is treated as a well-formed frame carrying nothing, so it is neither surfaced nor counted. Both are fixed by validating the shape rather than asserting it — which is what your proposed patch does, so this follows it.

Audit of the remaining SSE parsers — no other site is affected. Eight further data-frame parsers were checked; all parse into unknown and gate on a local record predicate before any dereference, so they were already correct:

src/chat/outbound.ts:510 and :678 (isRec), src/claude/outbound.ts:624 and :869 (isRec), src/server/claude-messages.ts:177 (isRec), src/web-search/anthropic-executor.ts:78 (isRec), src/vision/anthropic-describe.ts:63 (isRecord), src/adapters/openai-responses.ts:1220 (isPlainObject).

Fix

Validate the parse result instead of asserting it, at all four sites. The guard is written against the shape (null, non-object, array) rather than against null alone, so the silent-empty case is closed together with the crash.

Each site keeps its own existing malformed-frame handling rather than acquiring a new one — this PR changes what counts as malformed, not what happens next:

  • src/adapters/openai-chat.ts, src/adapters/google.ts — emit the existing malformed upstream SSE data frame error and terminate, exactly as the invalid-JSON path does.
  • src/adapters/anthropic.tsdebugDroppedFrame and continue, so the pre-existing message_stop truncation check still decides the stream's outcome.
  • src/web-search/parse.ts — warn and skip the frame.

The guard uses the shape-check form already present in these files (the wrapped checks in src/adapters/google.ts, the rawChoice check in src/adapters/openai-chat.ts), so no new helper or import is introduced.

Tests

New tests/sse-null-data-frame.test.ts — 25 tests, covering all four production sites against all five non-record payloads (null, 42, "text", true, []).

Assertions are written as parity against the syntactically-invalid control ({not json}) rather than by re-encoding each parser's terminal message. That states the actual requirement — a valid-JSON non-record frame is handled exactly like an unparseable one — and does not drift when those messages change. The control also proves the pre-existing guard still works, so a regression breaking both would not pass silently.

Beyond the shape matrix:

  • a non-record frame followed by data: [DONE] stays terminal on openai-chat and does not become a clean completion;
  • a non-record frame on anthropic does not prevent a later well-formed frame from being read through to done;
  • a non-record frame in the web-search sidecar does not discard the answer that follows it.

Verification on this branch (all runs via bun scripts/test.ts, i.e. --isolate plus the isolated CODEX_HOME/OPENCODEX_HOME environment):

  • new file 25/25 pass — and 17 of the 25 fail on dev without the production change, so the coverage is proven to bite rather than merely to pass;
  • 1103 pass / 0 fail across 61 files — every SSE, streaming, adapter, web-search, vision and chat/claude endpoint suite in the tree, i.e. the full blast radius of these four files;
  • bun x tsc --noEmit — output byte-identical to a stashed clean-dev control (both exit 2 on one pre-existing @napi-rs/keyring module-resolution error local to my machine);
  • bun scripts/privacy-scan.ts — passed.

On the Windows leg. This was developed on Windows, where the suite is known-red — #1059 tracks ~207 pre-existing failures and the leg is dispatch-only. Rather than report that as noise, I controlled the affected area directly: the 74 codex-* and cli-* files that contain every failure were run on this branch and on a stashed clean dev.

branch:     115 failing tests
clean dev:  115 failing tests
diff:       identical — same tests, both sides

So this change adds no failure to the known-red set, and none of those files is reachable from the four it touches.

Notes

One behavioural change beyond the crash fix. On openai-chat and google, a data: frame carrying a non-null non-record (42, "text", true, []) is now reported as a malformed frame and terminates the stream, where before it was silently ignored. This is deliberate — such a frame is not valid in either wire format, and silently continuing is how a broken stream reaches the truncation guard with a misleading cause. anthropic and the web-search sidecar keep their non-terminal handling, so nothing that previously completed a turn now fails.

No payload content is added to any log. The new src/web-search/parse.ts branch reports only the frame length and its JSON shape (null / array / number / …), not a slice of the payload. The pre-existing invalid-JSON warning one line above still logs payload.slice(0, 120); that line is untouched, but the new path deliberately does not copy the pattern.

Overlap with #1194. That PR changes the data: field-prefix handling in src/adapters/openai-chat.ts and src/web-search/parse.ts, among others. The changes are adjacent but disjoint — #1194 decides which lines become payloads, this PR decides what a parsed payload must look like. Whichever lands first, the other is a trivial rebase; happy to rebase onto #1194 if you would rather take them in that order.

The reporter also asked for a debugProviderDiagnostic on the malformed-frame path, noting the failure leaves no server-side trace. That is deliberately not in this PR — it is a diagnostics change with its own shape (which channel, at what level, and whether frame content may be recorded), and it should not ride along with a crash fix. Happy to take it as a follow-up, or to leave it to @brunoflma who raised it.

Review readiness checklist

This PR stays in draft until every box below is ticked. Tick all four boxes once the requirements are met:

  • All CI tests are green on my local testing.

  • I pushed my PR to the latest dev commit.

  • I resolved all correct Codex and CodeRabbit findings.

  • My PR is ready for review.

Summary by CodeRabbit

  • Bug Fixes

    • Improved streaming response handling when event data is null, a primitive value, or an array.
    • Prevented malformed or unexpected streaming frames from causing crashes.
    • Ensured invalid frames are safely logged and handled consistently across AI and search integrations.
  • Tests

    • Added coverage for malformed and unexpected streaming payloads.
    • Verified subsequent valid events continue processing.
    • Confirmed diagnostic warnings do not expose event payload contents.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

✅ READY

  • all PR quality gates passed; the review readiness checklist is complete.

Review readiness checklist

  • ✅ All CI tests are green on my local testing.
  • ✅ I pushed my PR to the latest dev commit.
  • ✅ I resolved all correct Codex and CodeRabbit findings.
  • ✅ My PR is ready for review.

4/4 boxes ticked.

This pull request has been marked Ready for Review.
CodeRabbit/Codex review was requested via the review-ready label. If no review appears, comment @coderabbitai review to request one.
Maintainers notified: @lidge-jun @Ingwannu @Wibias

Hygiene

Deterministic PR hygiene checks passed.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Aug 7, 2026
@github-actions
github-actions Bot marked this pull request as draft August 7, 2026 21:07
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bd3d8454-9cac-4b35-b421-0dc3d2b6c5e5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The SSE parsers now validate parsed JSON as non-null objects before reading event fields. Invalid shapes use parser-specific handling. Tests cover null, scalar, and array frames across all affected parsers.

Changes

SSE shape validation

Layer / File(s) Summary
Parser payload validation
src/adapters/anthropic.ts, src/adapters/google.ts, src/adapters/openai-chat.ts, src/web-search/parse.ts
The parsers reject null, scalar, and array JSON frames before accessing type. Provider parsers retain their existing malformed-frame behavior. The sidecar parser logs shape-specific warnings without exposing payload content.
Non-record frame regression tests
tests/sse-null-data-frame.test.ts
Tests cover invalid JSON parity, parser-specific terminal behavior, continued processing, warning-content hygiene, and preservation of later valid events.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

  • Issue 1219 — Directly addresses SSE parsing crashes caused by non-object JSON payloads in the affected adapters.

Possibly related PRs

Suggested reviewers: lidge-jun, wibias

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
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 (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: SSE data frames with non-record JSON payloads are treated as malformed.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/web-search/parse.ts`:
- Around line 156-169: Update the malformed-JSON warning in the SSE parsing flow
around parseSidecarSSE to omit payload.slice(0, 120) entirely, logging only the
frame length and a fixed malformed-JSON classification. Add a regression test
using a unique payload marker and verify console.warn does not receive or
contain that marker, while preserving the existing early return.
🪄 Autofix

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 Plus

Run ID: da7c462f-5ac4-401c-8acd-723657d9f536

📥 Commits

Reviewing files that changed from the base of the PR and between 9326b85 and 48f55bf.

📒 Files selected for processing (5)
  • src/adapters/anthropic.ts
  • src/adapters/google.ts
  • src/adapters/openai-chat.ts
  • src/web-search/parse.ts
  • tests/sse-null-data-frame.test.ts

Comment thread src/web-search/parse.ts
@snowyukitty
snowyukitty force-pushed the fix/adapters-null-sse-frame branch from 48f55bf to b01a623 Compare August 7, 2026 22:35
@github-actions
github-actions Bot marked this pull request as ready for review August 7, 2026 22:36
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

@github-actions[bot] The readiness gate is complete. I will review pull request #1240.

⚠️ Action not completed

Already reviewed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

@github-actions[bot] The readiness gate is complete. I will review pull request #1240.

⚠️ Action not completed

Already reviewed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/sse-null-data-frame.test.ts`:
- Around line 145-146: Remove the `event.type === "text"` comparison from the
events assertion in the SSE null-data-frame test, leaving only the valid
`"text_delta"` discriminator while preserving the existing `"done"` event
assertion.
🪄 Autofix

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 Plus

Run ID: ed8febd5-4d97-479b-a104-b4bcf61f4071

📥 Commits

Reviewing files that changed from the base of the PR and between 48f55bf and b01a623.

📒 Files selected for processing (3)
  • src/adapters/openai-chat.ts
  • src/web-search/parse.ts
  • tests/sse-null-data-frame.test.ts

Comment thread tests/sse-null-data-frame.test.ts Outdated
@snowyukitty
snowyukitty force-pushed the fix/adapters-null-sse-frame branch from b01a623 to 8585263 Compare August 7, 2026 23:51
@github-actions
github-actions Bot marked this pull request as draft August 7, 2026 23:52
@snowyukitty
snowyukitty force-pushed the fix/adapters-null-sse-frame branch from 8585263 to f155138 Compare August 7, 2026 23:56
@github-actions
github-actions Bot marked this pull request as ready for review August 7, 2026 23:57
@snowyukitty

Copy link
Copy Markdown
Contributor Author

Rebased onto current dev (396a0098) at f155138c — the readiness gate resets on every force-push, so this is the re-tick, not a second submission. No conflicts; dev has not touched any of the four files since this branch was cut.

Two review findings from CodeRabbit are resolved, both in-thread:

  1. Major, security/privacy — the pre-existing console.warn(… payload.slice(0, 120)) in src/web-search/parse.ts was copying untrusted frame content into the log, against .coderabbit.yaml's src/** rule. Both warnings in that function now report length plus a classification only, covered by a regression test parameterised over both warning paths. Verified it bites: restoring the slice fails the unparseable case.
  2. Minor, correctness — a dead event.type === "text" disjunct in the new test; AdapterEvent has no such member. Removed. Worth flagging that tsconfig.json is "include": ["src"], so tests/** is never typechecked — a green tsc cannot catch an impossible discriminator comparison in a test file. I have not changed that; it is a repo-wide decision, not something to slip into a bug fix.

One disclosure on the "CI green locally" box. tests/codex-app-server-processes.test.ts has one failing test on my machine:

warnIfStaleCodexAppServersAfterStartupWrite (#1046)
  > a defaulted read is memoized, and invalidation is what clears it

It is not from this change. Controlled by checking out upstream/dev detached at 396a0098 with this commit absent — it fails there identically (29 pass / 1 fail, same test). It is also outside this PR's reach: nothing here touches Codex app-server code. Everything else is green — 1149 pass / 1 fail across 63 files covering every SSE, streaming, adapter, web-search, vision and chat/claude endpoint suite, plus 27/27 on the new file, tsc --noEmit byte-identical to a clean-dev control, and privacy-scan passing.

Flagging it rather than quietly ticking the box, since I cannot honestly claim a fully green local run at this dev tip.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

@github-actions[bot] The readiness gate is complete. I will review pull request #1240.

⚠️ Action not completed

No files to review.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Ingwannu Ingwannu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

The crash diagnosis is valid, and the Anthropic plus web-search shape guards have the right non-terminal behavior. The OpenAI Chat and Google behavior must change before merge, though.

The reporter supplied a real healthy stream where data: null appears between answer deltas and the later finish_reason / [DONE]. On this head, both adapters classify that frame like invalid JSON and terminate immediately, so the reported provider still fails even though the TypeError is gone. The new tests currently lock in that incorrect behavior (is a terminal malformed frame and parity with {not json}).

Please make valid-JSON non-record frames return continue in openai-chat.ts and google.ts, keep sawAnyFrame = true after the Google shape guard, and replace the terminal-parity assertions with both of these regressions:

  • a mid-stream data: null followed by a valid finish chunk and [DONE] completes successfully;
  • a stream containing only non-record frames still reaches the existing truncation/empty-stream error, so skipping does not create a silent success.

Keep Anthropic/web-search non-terminal and payload-safe as they are. Also rebase the result onto current dev; this head is now 22 commits behind. I can re-review once the corrected semantics and fresh-base CI are present.

`JSON.parse("null")` returns null rather than throwing, so a try/catch around
the parse cannot see it. Four SSE frame parsers cast the result straight to
`Record<string, unknown>` and dereferenced it on the next line, so a `data: null`
frame walked past the malformed-frame guard and crashed the parser mid-stream.

Validate the shape at each site instead of asserting it. A non-record frame is
skipped, not terminated on: the reporter's capture shows `data: null` arriving as
a benign padding frame BETWEEN content deltas, with the legitimate finish_reason
chunk and [DONE] right behind it, so failing there discards an answer that has
already fully arrived — they measured the complete text landing and the turn
failing anyway. openai-chat and google now `continue`; anthropic drops the frame
so the message_stop check still governs; the web-search sidecar warns and skips.
An unparseable frame stays terminal in every parser, unchanged.

Skipping cannot mask a broken stream. A stream carrying only non-record frames
sets neither `finishReason` nor `sawUserFacingOutput` in openai-chat, and the
google guard returns before `sawAnyFrame`, so both still fail closed on the
existing EOF terminal-signal check. Both properties are covered by regressions.

`src/web-search/parse.ts` is not named in the report. It has the identical
defect and sits in the web-search path the reporter was actually running.
The other eight SSE data-frame parsers were audited and are already correct:
they parse into `unknown` and gate on a record predicate before dereferencing.

Neither web-search warning copies the frame's content. The new non-record branch
reports length and JSON shape only, and the pre-existing malformed-JSON warning
one line above stops logging `payload.slice(0, 120)`: an upstream SSE payload can
carry model output or credential material, and a frame that failed to parse is
the least trustworthy content there is. A regression test asserts that a unique
marker in the payload never reaches `console.warn` on either path.

Closes lidge-jun#1219
@snowyukitty
snowyukitty force-pushed the fix/adapters-null-sse-frame branch from f155138 to 965dd99 Compare August 8, 2026 06:21
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Current HEAD =='
git rev-parse HEAD

echo '== Changed files relative to merge base with dev =='
base="$(git merge-base HEAD dev 2>/dev/null || git merge-base HEAD origin/dev)"
git diff --stat "$base"...HEAD
git diff --unified=80 "$base"...HEAD -- \
  src/adapters/openai-chat.ts \
  src/adapters/google.ts \
  src/adapters/anthropic.ts \
  src/web-search/parse.ts \
  tests/sse-null-data-frame.test.ts

echo '== Structural outlines =='
ast-grep outline src/adapters/openai-chat.ts --items all --view expanded
ast-grep outline src/adapters/google.ts --items all --view expanded
ast-grep outline src/adapters/anthropic.ts --items all --view expanded
ast-grep outline src/web-search/parse.ts --items all --view expanded
ast-grep outline tests/sse-null-data-frame.test.ts --items all --view expanded

echo '== Existing parser malformed-frame handling and tests =='
rg -n -C 6 --glob '*.ts' \
  '(JSON\.parse|malformed|unparseable|Invalid .*frame|parseSidecarSSE|data\.type|data\.choices)' \
  src/adapters/openai-chat.ts src/adapters/google.ts src/adapters/anthropic.ts src/web-search/parse.ts tests/sse-null-data-frame.test.ts

Length of output: 1627


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== HEAD metadata =='
git show -s --format='commit=%H%nparents=%P%nsubject=%s' HEAD

echo '== Local and remote refs =='
git branch -a
git tag --contains HEAD || true
git remote -v || true

echo '== Commit ancestry and changed files =='
git diff-tree --no-commit-id --name-status -r HEAD
git diff --stat HEAD^ HEAD
git diff --unified=70 HEAD^ HEAD -- \
  src/adapters/openai-chat.ts \
  src/adapters/google.ts \
  src/adapters/anthropic.ts \
  src/web-search/parse.ts \
  tests/sse-null-data-frame.test.ts

echo '== Relevant source regions at HEAD =='
for file in \
  src/adapters/openai-chat.ts \
  src/adapters/google.ts \
  src/adapters/anthropic.ts \
  src/web-search/parse.ts \
  tests/sse-null-data-frame.test.ts
do
  echo "----- $file -----"
  rg -n -C 12 '(JSON\.parse|parseSidecarSSE|data\.type|data\.choices|malformed|unparseable)' "$file" || true
done

Length of output: 84967


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== OpenAI Chat: handler call sites and terminal path =='
sed -n '940,1160p' src/adapters/openai-chat.ts

echo '== Google: handler call sites and terminal path =='
sed -n '490,710p' src/adapters/google.ts

echo '== Anthropic: dropped-frame helper and stream terminal path =='
rg -n -C 10 'function debugDroppedFrame|const debugDroppedFrame|debugDroppedFrame\s*=|emitDone|message_stop|finally' src/adapters/anthropic.ts
sed -n '960,1150p' src/adapters/anthropic.ts

echo '== Web-search: complete parser and payload logging rules =='
sed -n '141,250p' src/web-search/parse.ts
fd -HI '^\.coderabbit\.ya?ml$|^coderabbit\.ya?ml$' . -x sh -c 'echo "----- $1 -----"; cat "$1"' sh {} \;

echo '== Direct uses of the changed diagnostic helper =='
rg -n -C 3 '\bdebugDroppedFrame\s*\(' src --glob '*.ts'

Length of output: 50516


@github-actions[bot] I completed a focused review of the available checkout.

I found no blocking issues in the inspected SSE parser changes.

  • The guards in src/adapters/openai-chat.ts, src/adapters/google.ts, and src/adapters/anthropic.ts run before record property access.
  • The new paths preserve each parser’s existing terminal behavior.
  • src/web-search/parse.ts does not copy untrusted payload content into warnings.
  • tests/sse-null-data-frame.test.ts covers all five non-record JSON types and stream-continuation behavior.

The sandbox checkout was commit 965dd990114fc6203297475142a28fcd7cb44642, not the readiness comment’s stated f155138c60145cb61e003c9028f616d88204b10d. I could not execute tests in this environment.

⚠️ Action not completed

Already reviewed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@snowyukitty

snowyukitty commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

You and @brunoflma are right, and I was wrong. Corrected in 965dd990, rebased onto current dev.

What I got wrong. I read #1219's body and its synthetic repro, treated data: null as a protocol violation, and made it terminal on purpose — I even wrote it up in the PR body as "one behavioural change beyond the crash fix" and argued for it. What I failed to do was re-read the issue before opening the PR. @brunoflma had already retracted their own proposed patch at 15:32Z with the upstream capture, nearly six hours before I submitted. I saw the comment count go 1 → 2 in later checks and never opened it. The capture settles it: data: null is padding between content deltas with finish_reason: "stop" and [DONE] right behind it, and their measurement — full PONG received, turn failed — is exactly the outcome my patch would have shipped.

There was also a tell inside my own diff that I should have caught without the capture: I made anthropic and web-search non-terminal and openai-chat/google terminal, for the same input class, in the same commit. That asymmetry had no justification.

Changes

  • src/adapters/openai-chat.ts, src/adapters/google.ts — non-record frames now return "continue".
  • The google guard returns before sawAnyFrame = true, as you asked, so padding is never counted as a frame.
  • anthropic and web-search untouched — still non-terminal, still payload-safe.
  • An unparseable frame stays terminal in every parser. That distinction is now asserted, not incidental.

Tests — replaced the terminal-parity assertions, and added a third case beyond the two you asked for:

  1. a mid-stream non-record frame in @brunoflma's exact captured stream → completes, text is PONG;
  2. a stream of only non-record frames → still fails closed (openai-chat trips !sawFinish && !sawUserFacingOutput, google trips !sawAnyFrame || !sawTerminalSignal);
  3. unparseable vs non-record at the same position in the same stream — the former still kills it, the latter does not.

All three run across null, 42, "text", true, []. Revert-check: restoring terminate fails 12 of the 36, so the coverage discriminates rather than just passing.

Verification (via bun scripts/test.ts, rebased onto d55b903d)

  • new file 36/36;
  • 1163 pass / 1 fail across 64 files — every SSE, streaming, adapter, web-search, vision and chat/claude endpoint suite. The 1 is vision-reasoning-contract, which passes 5/5 twice in isolation — load-order flake, not a regression;
  • codex-app-server-processes: 38 pass / 1 fail on this branch and 38 pass / 1 fail on a detached upstream/dev with my commit absent — same test (#1046), so pre-existing;
  • tsc --noEmit byte-identical to a clean-dev control; privacy-scan passed.

Not claiming a fully green local run: the #1046 failure above is real and reproduces without my change.


Two nested defects of the same class, deliberately NOT in this PR. While confirming the skip semantics I checked one level deeper than the frame root. These are unguarded in the same way and reproduce today:

{"candidates":[null]}                             → google.ts   null is not an object (evaluating 'candidates[0].finishReason')
{"choices":[{"delta":{"tool_calls":{"a":1}}}]}    → openai-chat  {} is not iterable
{"choices":[{"delta":{"tool_calls":[null]}}]}     → openai-chat  null is not an object (evaluating 'tc.index')

candidates and delta.tool_calls are both accepted by assertion. Unlike data: null I have no evidence any provider emits these, so they are a different risk tier and I did not want to widen a PR you are waiting to re-review. Happy to open a separate issue or PR, or leave them to you — your call.

@Ingwannu — ready for re-review when you have a window.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/sse-null-data-frame.test.ts`:
- Around line 19-22: Update the comment above the parity assertions in the test
to remove the claim that every valid-JSON non-record frame is handled like
unparseable JSON. Describe the parser-specific behavior instead: malformed JSON
terminates the stream, while OpenAI Chat and Google skip non-record frames and
continue processing later frames; keep the existing assertions unchanged.
🪄 Autofix

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 Plus

Run ID: 9317767a-d9d1-47d2-b94e-034e62fa6295

📥 Commits

Reviewing files that changed from the base of the PR and between 8585263 and 965dd99.

📒 Files selected for processing (3)
  • src/adapters/google.ts
  • src/adapters/openai-chat.ts
  • tests/sse-null-data-frame.test.ts

Comment on lines +19 to +22
// The syntactically-invalid control. It was already handled correctly before this fix; every
// assertion below is written as parity against it so the test states the actual requirement —
// a valid-JSON non-record frame is treated exactly like an unparseable one — rather than
// re-encoding each adapter's terminal message and drifting when those messages change.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the malformed-frame parity comment.

Lines 19-22 state that every non-record frame is treated like invalid JSON. This is false for the OpenAI Chat and Google adapters. Lines 121-129 and 151-159 verify that malformed JSON terminates the stream, while a non-record frame is skipped and later frames continue.

Describe parser-specific handling instead.

Proposed fix
-// assertion below is written as parity against it so the test states the actual requirement —
-// a valid-JSON non-record frame is treated exactly like an unparseable one — rather than
-// re-encoding each adapter's terminal message and drifting when those messages change.
+// assertion below preserves each parser's established malformed-frame behavior. A valid-JSON
+// non-record frame is skipped or dropped according to that parser's behavior, while invalid JSON
+// keeps its existing error handling.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// The syntactically-invalid control. It was already handled correctly before this fix; every
// assertion below is written as parity against it so the test states the actual requirement —
// a valid-JSON non-record frame is treated exactly like an unparseable one — rather than
// re-encoding each adapter's terminal message and drifting when those messages change.
// The syntactically-invalid control. It was already handled correctly before this fix; every
// assertion below preserves each parser's established malformed-frame behavior. A valid-JSON
// non-record frame is skipped or dropped according to that parser's behavior, while invalid JSON
// keeps its existing error handling.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/sse-null-data-frame.test.ts` around lines 19 - 22, Update the comment
above the parity assertions in the test to remove the claim that every
valid-JSON non-record frame is handled like unparseable JSON. Describe the
parser-specific behavior instead: malformed JSON terminates the stream, while
OpenAI Chat and Google skip non-record frames and continue processing later
frames; keep the existing assertions unchanged.

@lidge-jun
lidge-jun merged commit 2f0dc7c into lidge-jun:dev Aug 8, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working review-ready

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants