[GH-ISSUE #5666] [Bug] Malformed WebSocket frame opcodes are reported as continuation frames #3428

Closed
opened 2026-10-31 00:40:22 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @morluto on GitHub (Oct 20, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5666

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Process capture or runtime observation

Observed behavior

The CDP capture owner reports a missing or wrong-typed WebSocket frame opcode as numeric 0 without marking frame coverage incomplete. A string opcode "1" also causes payload "text" to be interpreted as base64 and reported as 3 bytes.

Reproduction

Pinned Node.js 24.18.0; source-only tsx probe, no browser, build, target JavaScript, CLI, or MCP call. Probe instantiates the real CdpCaptureEvents parser, ingests a same-origin Network.webSocketCreated event, then Network.webSocketFrameReceived events with missing opcode, string opcode "1", and valid numeric opcode 0.

Command:

node_modules/.bin/tsx -e 'import {inspectWebPageInputSchema} from "./src/domain/browserObservation.ts"; import {CdpCaptureEvents} from "./src/browser/CdpCaptureEvents.ts"; const origin="https://example.test"; const e=new CdpCaptureEvents(inspectWebPageInputSchema.parse({cdp_endpoint:"http://127.0.0.1:9222",allowed_origins:[origin],target_id:"page"}),new Set([origin])); e.ingest({method:"Network.webSocketCreated",params:{requestId:"ws",url:"wss://example.test/socket"}}); for (const [label,response] of [["missing",{payloadData:""}],["wrong-type",{opcode:"1",payloadData:"text"}],["zero-control",{opcode:0,payloadData:""}]] as const){e.ingest({method:"Network.webSocketFrameReceived",params:{requestId:"ws",response}}); console.log(JSON.stringify({label,event:e.webSocketConnectionsById.get("ws")?.events.at(-1),excluded:e.completeness.snapshot().excluded}));}'

Actual output:

{"label":"missing","event":{"direction":"received","opcode":0,"payload_bytes":0,"payload_shape":null},"excluded":[]}
{"label":"wrong-type","event":{"direction":"received","opcode":0,"payload_bytes":3,"payload_shape":null},"excluded":[]}
{"label":"zero-control","event":{"direction":"received","opcode":0,"payload_bytes":0,"payload_shape":null},"excluded":[]}

Host default node was v25.1.0; rerun explicitly pinned to v24.18.0 and confirmed the same output. No runtime/browser frequency claims.

Expected behavior

Preserve observed valid integer opcodes, including zero. Keep missing/invalid opcode unknown, mark websocket_frames / invalid_protocol_value, and avoid deriving text/binary payload semantics from a fabricated value.

Impact and scope

Controlled source-boundary reproduction only; real Chromium incidence and frequency are unknown. webCaptureDiff hashes direction/opcode sequences, so the fabricated continuation opcode can also affect comparison facts. No real browser or target execution was performed.

REA version

Source 690fc518540bae982038a74f47ef7781201785aa (6.7.0).

Runtime and platform

Node.js 24.18.0; macOS Darwin arm64.

Target and artifact context

Inert synthetic same-origin CDP WebSocket events.

Interface

Other: production CdpCaptureEvents source boundary; public CLI/MCP not run.

Earliest failure stage

Capture or observation

Relevant output or evidence

Producer at the examined revision uses Math.max(0, Math.trunc(numberValue(response?.opcode) ?? 0)). Stream comparison consumes the resulting opcode. Exact output is included in the reproduction above.

Typed outcome or recovery expected

Represent unknown opcode/payload interpretation explicitly and preserve independent observed facts.

Analysis or suspected cause

Source-derived: numeric fallback and truncation/clamping convert unknown producer data to plausible protocol values. The existing invalid-protocol coverage handles malformed base64 but misses malformed opcode.

Additional context

Distinct from #5656 (console timestamps), #5665 (Page navigation identity), and #5609 (lifecycle windows). Open PR #4040 at b9023cc2762d5cb53156f6ec8ea002e039487d73 does not modify this handler/schema. Issue/PR search found no opcode-specific duplicate. First bad revision and last good revision: NOT_ENOUGH_INFO; history was not bisected. AI-assisted source audit.

Originally created by @morluto on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5666 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Process capture or runtime observation ### Observed behavior The CDP capture owner reports a missing or wrong-typed WebSocket frame opcode as numeric `0` without marking frame coverage incomplete. A string opcode `"1"` also causes payload `"text"` to be interpreted as base64 and reported as 3 bytes. ### Reproduction Pinned Node.js 24.18.0; source-only `tsx` probe, no browser, build, target JavaScript, CLI, or MCP call. Probe instantiates the real `CdpCaptureEvents` parser, ingests a same-origin `Network.webSocketCreated` event, then `Network.webSocketFrameReceived` events with missing opcode, string opcode `"1"`, and valid numeric opcode `0`. Command: ```sh node_modules/.bin/tsx -e 'import {inspectWebPageInputSchema} from "./src/domain/browserObservation.ts"; import {CdpCaptureEvents} from "./src/browser/CdpCaptureEvents.ts"; const origin="https://example.test"; const e=new CdpCaptureEvents(inspectWebPageInputSchema.parse({cdp_endpoint:"http://127.0.0.1:9222",allowed_origins:[origin],target_id:"page"}),new Set([origin])); e.ingest({method:"Network.webSocketCreated",params:{requestId:"ws",url:"wss://example.test/socket"}}); for (const [label,response] of [["missing",{payloadData:""}],["wrong-type",{opcode:"1",payloadData:"text"}],["zero-control",{opcode:0,payloadData:""}]] as const){e.ingest({method:"Network.webSocketFrameReceived",params:{requestId:"ws",response}}); console.log(JSON.stringify({label,event:e.webSocketConnectionsById.get("ws")?.events.at(-1),excluded:e.completeness.snapshot().excluded}));}' ``` Actual output: ```json {"label":"missing","event":{"direction":"received","opcode":0,"payload_bytes":0,"payload_shape":null},"excluded":[]} {"label":"wrong-type","event":{"direction":"received","opcode":0,"payload_bytes":3,"payload_shape":null},"excluded":[]} {"label":"zero-control","event":{"direction":"received","opcode":0,"payload_bytes":0,"payload_shape":null},"excluded":[]} ``` Host default `node` was v25.1.0; rerun explicitly pinned to v24.18.0 and confirmed the same output. No runtime/browser frequency claims. ### Expected behavior Preserve observed valid integer opcodes, including zero. Keep missing/invalid opcode unknown, mark `websocket_frames / invalid_protocol_value`, and avoid deriving text/binary payload semantics from a fabricated value. ### Impact and scope Controlled source-boundary reproduction only; real Chromium incidence and frequency are unknown. `webCaptureDiff` hashes direction/opcode sequences, so the fabricated continuation opcode can also affect comparison facts. No real browser or target execution was performed. ### REA version Source `690fc518540bae982038a74f47ef7781201785aa` (6.7.0). ### Runtime and platform Node.js 24.18.0; macOS Darwin arm64. ### Target and artifact context Inert synthetic same-origin CDP WebSocket events. ### Interface Other: production `CdpCaptureEvents` source boundary; public CLI/MCP not run. ### Earliest failure stage Capture or observation ### Relevant output or evidence [Producer at the examined revision](https://github.com/morluto/rea/blob/690fc518540bae982038a74f47ef7781201785aa/src/browser/CdpCaptureEventHandlers.ts#L502-L524) uses `Math.max(0, Math.trunc(numberValue(response?.opcode) ?? 0))`. [Stream comparison](https://github.com/morluto/rea/blob/690fc518540bae982038a74f47ef7781201785aa/src/domain/webCaptureDiff.ts#L579-L586) consumes the resulting opcode. Exact output is included in the reproduction above. ### Typed outcome or recovery expected Represent unknown opcode/payload interpretation explicitly and preserve independent observed facts. ### Analysis or suspected cause Source-derived: numeric fallback and truncation/clamping convert unknown producer data to plausible protocol values. The existing invalid-protocol coverage handles malformed base64 but misses malformed opcode. ### Additional context Distinct from #5656 (console timestamps), #5665 (Page navigation identity), and #5609 (lifecycle windows). Open PR #4040 at `b9023cc2762d5cb53156f6ec8ea002e039487d73` does not modify this handler/schema. Issue/PR search found no opcode-specific duplicate. First bad revision and last good revision: NOT_ENOUGH_INFO; history was not bisected. AI-assisted source audit.
bsakaguchi 2026-10-31 00:40:22 +00:00
  • closed this issue
  • added the
    bug
    triage
    labels
Author
Owner

@CassieFox23 commented on GitHub (Oct 20, 2026):

TL;DR: This still happens on main on Linux. A frame with a missing or bad opcode is stored as opcode 0. The section is not marked incomplete. Node 22, 24 and 26 give the same output.

Checked on main dceefa22c (Node 22.23.1, 24.18.0 and 26.11.1). I used the same source-only probe as the report, with synthetic events and no browser. Each run sent one Network.webSocketCreated event, then three received frames.

Frame sent Opcode stored Payload bytes Section marked incomplete Same on Node 22, 24, 26
no opcode, payload "" 0 0 no yes
opcode "1" (a string), payload "text" 0 3 no yes
opcode 0 (a number), payload "" 0 0 no yes

Rows one and three store the same event. So a reader can't tell a missing opcode from a real continuation frame. In the second row the text "text" was read as base64 and counted as 3 bytes. The handler line is Math.max(0, Math.trunc(numberValue(response?.opcode) ?? 0)) in src/browser/CdpCaptureEventHandlers.ts. The excluded list stayed empty in all three rows.

I did not find an earlier comment on this issue or a commit that changes this line. The last commit to touch this file before main dceefa22c is 690fc51854.

Not run

  • A real browser, the CLI, MCP, or webCaptureDiff.
  • Sent frames (only received frames), and non-integer opcodes such as 1.5.
  • macOS.

Thanks, morluto, for the clear report and the ready-to-run probe.

Commands
node node_modules/tsx/dist/cli.mjs probe.ts

probe.ts is the report's one-liner as a file. It builds CdpCaptureEvents from inspectWebPageInputSchema.parse({cdp_endpoint:"http://127.0.0.1:9222",allowed_origins:["https://example.test"],target_id:"page"}). Then it ingests Network.webSocketCreated and the three Network.webSocketFrameReceived events from the table, and prints the last stored event plus completeness.snapshot().excluded. Exit 0 on each Node. I used npm ci --ignore-scripts and npm run build:cached (exit 0), but the probe runs from source through tsx, as the report does.

Output on Node 24.18.0, identical on 22.23.1 and 26.11.1:

{"label":"missing","event":{"direction":"received","opcode":0,"payload_bytes":0,"payload_shape":null},"excluded":[]}
{"label":"wrong-type","event":{"direction":"received","opcode":0,"payload_bytes":3,"payload_shape":null},"excluded":[]}
{"label":"zero-control","event":{"direction":"received","opcode":0,"payload_bytes":0,"payload_shape":null},"excluded":[]}

Each Node ran the probe once. Isolated tree under /tmp.

Test setup
  • Box: Nobara 44 (Fedora 44 base), Linux 7.2.11
  • CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads
  • RAM: 32 GB DDR4-3600
  • Node: 22.23.1, 24.18.0, 26.11.1
  • Head: dceefa22c
  • Other: another job shared the box during the run

🤖 Written and tested with Claude Code (Sonnet 5.5, verified by Opus 5.5), posted with my OK 🦊

<!-- gh-comment-id:6300959992 --> @CassieFox23 commented on GitHub (Oct 20, 2026): **TL;DR:** This still happens on main on Linux. A frame with a missing or bad opcode is stored as opcode 0. The section is not marked incomplete. Node 22, 24 and 26 give the same output. Checked on main dceefa22c (Node 22.23.1, 24.18.0 and 26.11.1). I used the same source-only probe as the report, with synthetic events and no browser. Each run sent one `Network.webSocketCreated` event, then three received frames. | Frame sent | Opcode stored | Payload bytes | Section marked incomplete | Same on Node 22, 24, 26 | | --- | --- | --- | --- | --- | | no opcode, payload `""` | 0 | 0 | no | yes | | opcode `"1"` (a string), payload `"text"` | 0 | 3 | no | yes | | opcode `0` (a number), payload `""` | 0 | 0 | no | yes | Rows one and three store the same event. So a reader can't tell a missing opcode from a real continuation frame. In the second row the text `"text"` was read as base64 and counted as 3 bytes. The handler line is `Math.max(0, Math.trunc(numberValue(response?.opcode) ?? 0))` in `src/browser/CdpCaptureEventHandlers.ts`. The `excluded` list stayed empty in all three rows. I did not find an earlier comment on this issue or a commit that changes this line. The last commit to touch this file before main dceefa22c is 690fc51854. ### Not run - A real browser, the CLI, MCP, or `webCaptureDiff`. - Sent frames (only received frames), and non-integer opcodes such as `1.5`. - macOS. Thanks, morluto, for the clear report and the ready-to-run probe. <details> <summary>Commands</summary> ```sh node node_modules/tsx/dist/cli.mjs probe.ts ``` `probe.ts` is the report's one-liner as a file. It builds `CdpCaptureEvents` from `inspectWebPageInputSchema.parse({cdp_endpoint:"http://127.0.0.1:9222",allowed_origins:["https://example.test"],target_id:"page"})`. Then it ingests `Network.webSocketCreated` and the three `Network.webSocketFrameReceived` events from the table, and prints the last stored event plus `completeness.snapshot().excluded`. Exit 0 on each Node. I used `npm ci --ignore-scripts` and `npm run build:cached` (exit 0), but the probe runs from source through `tsx`, as the report does. Output on Node 24.18.0, identical on 22.23.1 and 26.11.1: ```json {"label":"missing","event":{"direction":"received","opcode":0,"payload_bytes":0,"payload_shape":null},"excluded":[]} {"label":"wrong-type","event":{"direction":"received","opcode":0,"payload_bytes":3,"payload_shape":null},"excluded":[]} {"label":"zero-control","event":{"direction":"received","opcode":0,"payload_bytes":0,"payload_shape":null},"excluded":[]} ``` Each Node ran the probe once. Isolated tree under /tmp. </details> <details> <summary>Test setup</summary> - Box: Nobara 44 (Fedora 44 base), Linux 7.2.11 - CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads - RAM: 32 GB DDR4-3600 - Node: 22.23.1, 24.18.0, 26.11.1 - Head: dceefa22c - Other: another job shared the box during the run </details> 🤖 Written and tested with Claude Code (Sonnet 5.5, verified by Opus 5.5), posted with my OK 🦊
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3428
No description provided.