perf(plugin): Node IPC split-body binary framing for hook payloads#1192
Conversation
8 red tests for split-body binary framing that avoids JSON-encoding large HTML/content payloads in the Node IPC protocol: - EncodeMessage: 5 tests for typed payloads (HookRenderedPayload, HookFormatRenderedPayload, HookTransformPayload) and chained map results (html/content keys), plus single-header preservation for non-hook messages - DecodeMessage: 1 test for multi-header frame parsing with raw body injection into result map - Send: 1 test for multi-header response reading with raw body injection under X-Body-Field name PLAN.md and IMPLEMENTATION.md updated with split-body framing spec and implementation guidance. Refs #1181 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- P1 #6: Updated summary table (Phase 5: ~129, cumulative downstream) - P2 #1: Added multi-byte character table entry (CJK, emoji) — validates X-Body-Length is byte count not rune count - P2 #7: Added \r\n suffix to X-Body-Length assertion to prevent decimal-prefix false match (50 matching 500) - P3 #2: Use parsed headers map from parseSplitFrame, assert header value matches actual raw body byte count - P3 #3: Added empty split field test — falls back to single-header - P3 #4: Added dual-key map priority test — html over content - P3 #5: Added caller map immutability guard test 12 total tests (was 8). Test counts updated in IMPLEMENTATION.md (section header, node.go description, impl guidance, summary table). PLAN.md updated with empty-field fallback, dual-key priority, and map immutability constraints. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…g-spec test: spec tests for Node IPC binary framing (#1181)
#1181) Hook messages with large HTML/content fields now use split-body framing to avoid JSON-encoding the large string. The field is stripped from the JSON body and sent as raw bytes after it, with X-Body-Length and X-Body-Field headers describing the split. DecodeMessage and Send both parse the multi-header format and inject the raw bytes back into the result map. Non-hook messages and empty fields fall back to standard single-header framing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
✅ Deploy Preview for alloyssg ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 21 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughNode IPC framing now separates large hook ChangesNode IPC split-body framing
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Possibly related PRs
Sequence Diagram(s)sequenceDiagram
participant GoPlugin
participant bridge.js
participant Hook
participant NodeBridge
GoPlugin->>bridge.js: Send JSON metadata and raw html/content bytes
bridge.js->>Hook: Reconstruct hook payload
Hook-->>bridge.js: Return hook result
bridge.js-->>NodeBridge: Send JSON response and optional raw bytes
NodeBridge->>NodeBridge: Restore split field in result
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
internal/plugin/node.go (1)
139-181: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
stripFieldre-declares each payload's field list — new struct fields will silently vanish from split frames.The plan specifies marshal-to-JSON → unmarshal-to-map →
delete(key)precisely so the projection stays in sync with the struct definitions. Deriving the map generically also removes the duplicated type switch withsplitBodyField.♻️ Generic strip via JSON round-trip
func stripField(msg *Message, fieldName string) *Message { cp := *msg - switch p := msg.Payload.(type) { - case HookRenderedPayload: - m := map[string]interface{}{ - "frontMatter": p.FrontMatter, - "url": p.URL, - "path": p.Path, - } - cp.Payload = m - case HookFormatRenderedPayload: - ... - case map[string]interface{}: + switch p := msg.Payload.(type) { + case map[string]interface{}: m := make(map[string]interface{}, len(p)) for k, v := range p { if k != fieldName { m[k] = v } } cp.Payload = m + default: + raw, err := jsonCodec.Marshal(p) + if err != nil { + return &cp + } + var m map[string]interface{} + if err := jsonCodec.Unmarshal(raw, &m); err != nil { + return &cp + } + delete(m, fieldName) + cp.Payload = m } return &cp }Note this re-marshals the large field once; strip it from the struct first (or shallow-clear the field on a struct copy) if that cost matters.
🤖 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 `@internal/plugin/node.go` around lines 139 - 181, Update stripField to derive payload fields generically through JSON marshal and unmarshal into a map, then delete fieldName, instead of maintaining per-payload field lists in the HookRenderedPayload, HookFormatRenderedPayload, and HookTransformPayload cases. Preserve the existing map[string]interface{} handling and return a copied Message without mutating the original; align the projection with the struct definitions so newly added fields are retained.internal/plugin/node_test.go (1)
1101-1126: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd split-frame round-trip and malformed-length cases.
Two gaps the current specs leave open, both matching real defects in
internal/plugin/node.go:
EncodeMessage→DecodeMessageround-trip of a hook payload — the field currently lands inResult, not back inPayload.- Truncated frame / negative
X-Body-Length— currently panics rather than erroring.🤖 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 `@internal/plugin/node_test.go` around lines 1101 - 1126, The DecodeMessage split-body specs only cover injection into Result; add cases that round-trip a hook payload through EncodeMessage and DecodeMessage, asserting the split field is restored to Payload, and add malformed-frame cases for truncated bodies and negative X-Body-Length values, asserting DecodeMessage returns an error without panicking. Anchor the tests in the existing “DecodeMessage split-body parsing” Describe block and use the existing hook payload and EncodeMessage APIs.
🤖 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 `@internal/plugin/node.go`:
- Around line 919-936: Validate the parsed body length in the split-body
handling before allocating rawBody: reject negative or unreasonably large
X-Body-Length values using the same validation rule and established limit as
DecodeMessage. Return a descriptive error for invalid lengths, and only call
make and io.ReadFull after validation succeeds.
- Around line 199-228: Validate plugin-supplied frame lengths before using them:
in internal/plugin/node.go lines 199-228, reject negative contentLen/bodyLen and
ensure contentLen plus bodyLen does not exceed len(afterSep) before slicing; in
lines 919-936, reject negative bodyLen and enforce a sane maximum before
make([]byte, bodyLen). Apply these checks in the decoder and bridge reader paths
without changing valid-frame behavior.
- Around line 875-906: Update the response-header parsing loop to validate the
first header line specifically as a Content-Length header before accepting
generic key-value lines. Ensure stdout pollution such as “debug: loading plugin”
immediately returns stdoutPollutionErrFmt with its truncated snippet, while
preserving handling of valid Content-Length and X-Body-* headers.
- Around line 213-228: Update the split-body injection logic in the decoder
around msg.Result so it prefers an existing Result map, but when Result is nil
falls back to the message Payload map instead of synthesizing an empty Result.
Insert the body field into the selected map and preserve the decoded hook
request’s payload/result placement so EncodeMessage and DecodeMessage round-trip
correctly.
- Around line 103-137: Update the Node reader in bridge.js to parse
X-Body-Length and X-Body-Field headers alongside Content-Length, then consume
and reattach the raw body field after decoding the JSON payload. Preserve the
existing Content-Length-only path for standard frames, and ensure split-body
frames consume all bytes before reading the next message.
---
Nitpick comments:
In `@internal/plugin/node_test.go`:
- Around line 1101-1126: The DecodeMessage split-body specs only cover injection
into Result; add cases that round-trip a hook payload through EncodeMessage and
DecodeMessage, asserting the split field is restored to Payload, and add
malformed-frame cases for truncated bodies and negative X-Body-Length values,
asserting DecodeMessage returns an error without panicking. Anchor the tests in
the existing “DecodeMessage split-body parsing” Describe block and use the
existing hook payload and EncodeMessage APIs.
In `@internal/plugin/node.go`:
- Around line 139-181: Update stripField to derive payload fields generically
through JSON marshal and unmarshal into a map, then delete fieldName, instead of
maintaining per-payload field lists in the HookRenderedPayload,
HookFormatRenderedPayload, and HookTransformPayload cases. Preserve the existing
map[string]interface{} handling and return a copied Message without mutating the
original; align the projection with the struct definitions so newly added fields
are retained.
🪄 Autofix (Beta)
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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 199dc9f3-26b4-46d6-84d9-e23e10c8fdb5
📒 Files selected for processing (4)
internal/plugin/node.gointernal/plugin/node_test.goplans/IMPLEMENTATION.mdplans/PLAN.md
zeroedin
left a comment
There was a problem hiding this comment.
Code Review Results
Scope: internal/plugin/node.go (implementation), plans/PLAN.md + plans/IMPLEMENTATION.md (spec updates)
Intent: Add split-body binary framing to Node IPC to avoid JSON-encoding ~800KB HTML payloads in hook messages
P0 — Critical
| # | File | Issue | Reviewer | Confidence | Route |
|---|---|---|---|---|---|
| 1 | bridge.js (unchanged) |
bridge.js not updated — Go→Node split-body frames will corrupt the Node message stream. EncodeMessage now produces split-body frames for hook messages. Send() writes these to Node's stdin. bridge.js only parses Content-Length (line 75 regex), reads that many bytes as JSON, then advances the buffer — leaving the raw body bytes as garbage for the next message parse. Every CallHook/BatchCallHook with HTML content will corrupt the bridge. IMPLEMENTATION.md steps 4-5 specify bridge.js changes that are missing from this PR. |
adversarial + manual | 100 | Fix or gate |
| 2 | node.go:206,220 |
No bounds check on Content-Length/X-Body-Length before slicing. afterSep[:contentLen] panics if contentLen > len(afterSep). afterSep[contentLen:contentLen+bodyLen] panics if sum exceeds length. A truncated frame or lying header causes a runtime panic in DecodeMessage. |
adversarial | 100 | Fix |
| 3 | node.go:201,215,903,921 |
Negative Content-Length or X-Body-Length causes panic. strconv.Atoi("-1") succeeds. make([]byte, -1) panics in Send(). afterSep[:-1] panics in DecodeMessage. No non-negative validation after any Atoi call. |
adversarial | 100 | Fix |
P1 — High
| # | File | Issue | Reviewer | Confidence | Route |
|---|---|---|---|---|---|
| 4 | node.go:219,925 |
Missing X-Body-Field with present X-Body-Length injects under empty-string key. headers["X-Body-Field"] returns "" when absent. resultMap[""] = string(rawBody) pollutes the result map with a ghost key. Both DecodeMessage and Send have this path. |
adversarial | 100 | Fix |
| 5 | node.go:888 |
Stdout pollution detection regression. Old code required the first line to be Content-Length:. New header loop accepts ANY key: value line as a valid header. "DEBUG: processing request" from a plugin writes to headers["DEBUG"] silently. IMPLEMENTATION.md step 3 explicitly requires: "The first line must still be checked for the Content-Length: prefix." |
adversarial + spec | 80 | Fix |
| 6 | node.go:143-180 |
stripField diverges from spec. IMPLEMENTATION.md step 1 says "marshal the struct to JSON, unmarshal to map[string]interface{}, delete the key". Implementation manually constructs map literals, hardcoding field names from each payload struct. New fields added to HookTransformPayload etc. will be silently dropped — no compile-time enforcement, no test failure. |
maintainability | 88 | Fix per spec |
| 7 | node.go:213-228 |
DecodeMessage only injects into Result, never Payload. IMPLEMENTATION.md step 2 says "type-assert Result (or Payload if Result is nil)". The implementation only handles Result. Outbound frames (Go→Node) have the split field in Payload, not Result. Round-trip EncodeMessage → DecodeMessage loses the field. |
correctness + spec | 90 | Fix |
P2 — Moderate
| # | File | Issue | Reviewer | Confidence | Route |
|---|---|---|---|---|---|
| 8 | node.go:128 |
Unnecessary ~800KB allocation per encode. rawBytes := []byte(fieldValue) copies the entire HTML string to get byte length. len(string) already returns byte count in Go. Use len(fieldValue) for the header and append(frame, fieldValue...) directly. For a 1000-page batch, this is ~800MB of throwaway allocation. |
performance | 95 | Fix |
| 9 | node.go:187 |
string(data) copies entire frame including ~800KB raw body. DecodeMessage converts all input to string for header parsing, but only the header block (< 200 bytes) needs string conversion. No production callers today, but if wired into a hot path this is wasteful. |
performance | 70 | Advisory |
| 10 | node_test.go:7 |
encoding/json in test file. Tests import encoding/json instead of using jsonCodec/sonic. Pre-existing from spec PR — tests are immutable. Flagging for architect awareness: if sonic has different deserialization behavior, tests could pass with encoding/json but fail in production. |
standards | 100 | Architect issue |
P3 — Advisory
| # | File | Issue | Reviewer | Confidence | Route |
|---|---|---|---|---|---|
| 11 | node.go:888-891 |
Malformed protocol headers silently dropped. A header like X-Body-Length:42 (no space after colon) passes the HasPrefix check but fails SplitN — silently swallowed. For X-Body headers, this causes silent fallback to non-split-body mode, and the raw body bytes remain in the stream corrupting the next message. |
adversarial | 75 | Advisory |
| 12 | (missing) | No benchmarks for the perf feature. PR claims performance improvement but includes no benchmark comparing split-body vs. all-JSON at realistic sizes. | performance | 95 | Open issue |
Coverage
- Testing gaps: No tests for truncated frames (Content-Length lies), negative header values, missing X-Body-Field with present X-Body-Length,
EncodeMessage→DecodeMessageround-trip, or stdout pollution withkey: valueformat lines. Tests are spec-derived and immutable — open an issue for the architect. - Residual risks: After any protocol parse error in
Send(), thebufio.Readeris in an unknown state. SubsequentSend()calls on the same bridge will read stale bytes. No drain/reset/reconnect mechanism exists.
Verdict: Not ready
Reasoning: P0 #1 is a showstopper —
EncodeMessagesends split-body frames to Node's stdin, but bridge.js doesn't understand the format (steps 4-5 of IMPLEMENTATION.md are unimplemented). Every hook call with HTML content will corrupt the bridge protocol stream. P0 #2-3 are panic-on-malformed-input bugs that need bounds/sign validation. P1 #5-7 are spec deviations that need alignment. The Go-side implementation logic is sound in isolation, but it cannot ship without either (a) updating bridge.js to parse/produce split-body frames, or (b) gating split-body encoding so it only activates when bridge.js signals support (version handshake).
- P0 #1: Update bridge.js inbound parser to handle split-body frames (X-Body-Length/X-Body-Field headers) and outbound sendMessage to produce split-body frames for hook results with html/content fields - P0 #2-3: Add bounds and negative-value validation on Content-Length and X-Body-Length in both DecodeMessage and Send before slicing or allocating - P1 #4: Require X-Body-Field header when X-Body-Length is present; error on empty field name - P1 #5: Restore stdout pollution detection — first header line in Send must start with "Content-Length:", rejecting "debug: ..." lines - P1 #6: Switch stripField to JSON round-trip (marshal struct → unmarshal to map → delete key) per IMPLEMENTATION.md spec, so new struct fields are automatically included - P1 #7: DecodeMessage split-body injection prefers Result map, falls back to Payload map when Result is nil, per spec - P2 #8: Use len(fieldValue) directly in EncodeMessage instead of allocating []byte(fieldValue) copy for byte-length measurement Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Review responseAll fixes pushed in 531539d before these replies. P0 #1 — bridge.js not updatedFixed in 531539d. bridge.js inbound parser now reads P0 #2 — No bounds check before slicingFixed in 531539d. P0 #3 — Negative lengths cause panicFixed in 531539d. Same bounds checks as #2. P1 #4 — Missing X-Body-Field injects under empty keyFixed in 531539d. Both P1 #5 — Stdout pollution detection regressionFixed in 531539d. Added P1 #6 — stripField diverges from specFixed in 531539d. P1 #7 — DecodeMessage only injects into ResultFixed in 531539d. Injection logic now: (1) if P2 #8 — Unnecessary allocation in EncodeMessageFixed in 531539d. Replaced P2 #9 — string(data) copies entire frameSkipped. P2 #10 — encoding/json in test fileSkipped (architect issue). Tests are immutable. Opened #1195 for the architect to evaluate. P3 #11 — Malformed protocol headers silently droppedSkipped. The P3 #12 — No benchmarksSkipped (architect issue). Opened #1194 which covers benchmark specs among other edge case tests. Testing gaps (advisory)Opened #1194 for the architect — covers round-trip fidelity, truncated frames, negative lengths, missing X-Body-Field, and stdout pollution with |
There was a problem hiding this comment.
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 `@internal/plugin/bridge.js`:
- Around line 96-127: Validate the split-body headers immediately after
computing blMatch, bfMatch, splitBodyLen, and splitBodyField: reject frames
where exactly one of X-Body-Length or X-Body-Field is present, before
calculating totalLen or consuming buffer data. Surface the mismatch as a parsing
error, while preserving the existing split-body handling when both headers are
present and normal frames when neither is present.
🪄 Autofix (Beta)
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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 70ddaf72-c7d1-4ea2-81cc-0b5eeb17d727
📒 Files selected for processing (2)
internal/plugin/bridge.jsinternal/plugin/node.go
🚧 Files skipped from review as they are similar to previous changes (1)
- internal/plugin/node.go
, #1195) Add 7 edge case tests for split-body binary framing: round-trip fidelity, truncated frame rejection, negative X-Body-Length, missing X-Body-Field, stdout pollution in key:value format, and Send-path variants. Replace encoding/json with jsonutil.JSON (bytedance/sonic) in node_test.go to match production codec behavior. Refs #1194 Refs #1195 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Follow-up: issues #1194 and #1195Pushed in 3cbd51e. Issue #1195 — encoding/json → sonicReplaced Issue #1194 — 7 edge case tests added
All 308 plugin tests pass (301 pre-existing + 7 new). PLAN.md and IMPLEMENTATION.md updated with error handling spec and test counts. |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
internal/plugin/node_test.go (1)
1216-1273: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a case for a truncated split body (
X-Body-Lengthbeyond available bytes).The negative-length test only covers the
bodyLen < 0half of the bounds check inDecodeMessage(internal/plugin/node.go:bodyLen < 0 || contentLen+bodyLen > len(afterSep)). The overflow half — a realistic short-write/killed-child scenario — is unasserted, so a regression there would surface as a slice panic rather than an error.💚 Suggested additional spec
It("returns error when X-Body-Length exceeds bytes available after the JSON body", func() { jsonPart := `{"id":1,"result":{"status":"ok"}}` rawPart := "<p>short</p>" frame := fmt.Sprintf( "Content-Length: %d\r\nX-Body-Length: %d\r\nX-Body-Field: html\r\n\r\n%s%s", len(jsonPart), len(rawPart)+100, jsonPart, rawPart, ) _, err := plugin.DecodeMessage([]byte(frame)) Expect(err).To(HaveOccurred(), "DecodeMessage must reject a split-body frame whose raw body is truncated") Expect(err.Error()).To(ContainSubstring("X-Body-Length")) })🤖 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 `@internal/plugin/node_test.go` around lines 1216 - 1273, Add a DecodeMessage test in the “malformed split-body frames” specs covering X-Body-Length greater than the bytes available after the JSON body. Build a frame with X-Body-Field, a short raw body, and an intentionally oversized X-Body-Length; assert an error occurs and identifies X-Body-Length, ensuring truncation returns an error rather than panicking.
🤖 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.
Nitpick comments:
In `@internal/plugin/node_test.go`:
- Around line 1216-1273: Add a DecodeMessage test in the “malformed split-body
frames” specs covering X-Body-Length greater than the bytes available after the
JSON body. Build a frame with X-Body-Field, a short raw body, and an
intentionally oversized X-Body-Length; assert an error occurs and identifies
X-Body-Length, ensuring truncation returns an error rather than panicking.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3ddddaaf-b56e-4944-be65-60a3b1b75426
📒 Files selected for processing (3)
internal/plugin/node_test.goplans/IMPLEMENTATION.mdplans/PLAN.md
🚧 Files skipped from review as they are similar to previous changes (2)
- plans/PLAN.md
- plans/IMPLEMENTATION.md
zeroedin
left a comment
There was a problem hiding this comment.
Code Review Results (Re-review)
Scope: internal/plugin/node.go, internal/plugin/bridge.js, internal/plugin/node_test.go, plans/PLAN.md, plans/IMPLEMENTATION.md
Intent: Split-body binary framing for Node IPC hook payloads — avoids JSON-encoding ~800KB HTML
Previous findings disposition
| Prior # | Severity | Finding | Status |
|---|---|---|---|
| P0 #1 | Critical | bridge.js not updated | ✅ Fixed — inbound parser + outbound sendMessage both updated |
| P0 #2 | Critical | No bounds check on Content-Length/X-Body-Length | ✅ Fixed — node.go:191-192, node.go:212 |
| P0 #3 | Critical | Negative values cause panic | ✅ Fixed — node.go:191, 212, 916, 941 |
| P1 #4 | High | Missing X-Body-Field injects under empty key | ✅ Fixed — node.go:209-210, 938-939 |
| P1 #5 | High | Stdout pollution detection regression | ✅ Fixed — first flag + node.go:892-894 |
| P1 #6 | High | stripField manual maps vs spec | ✅ Fixed — default case uses JSON round-trip |
| P1 #7 | High | DecodeMessage only injects into Result | ✅ Fixed — node.go:217-228 with Payload fallback |
| P2 #8 | Moderate | Unnecessary []byte allocation |
✅ Fixed — len(fieldValue) + append(frame, fieldValue...) |
| P2 #10 | Moderate | encoding/json in tests |
✅ Fixed — replaced with jsonutil.JSON |
All P0 and P1 findings are resolved. New edge-case spec tests cover truncated frames, negative values, missing headers, stdout pollution in key: value format, and round-trip fidelity. CI passes. Race detector clean. 308 tests pass (19 for split-body framing).
P2 — Moderate
| # | File | Issue | Reviewer | Confidence | Route |
|---|---|---|---|---|---|
| 1 | node.go:154-164 |
stripField JSON round-trip marshals the full payload including the 800KB field being stripped. jsonCodec.Marshal(p) serializes the complete struct (including the large HTML), then Unmarshal builds a map, then delete(m, fieldName) removes it. Benchmark: 825µs / 4MB per encode on M2 Pro for 800KB HTML. For a 1000-page batch across 12 cores, that's ~69s of CPU time and ~4GB of temporary allocation just for the round-trip that exists to remove one field. The prior manual-map approach only copied the small metadata fields (~200 bytes), avoiding all serialization of the large field. Recommendation: Restore per-type manual map construction for the three known types as the fast path; keep the JSON round-trip default case as a future-proof fallback for unknown types. This gives both maintainability (new types auto-work) and performance (known types stay fast). |
performance | 85 | Fix |
P3 — Advisory
| # | File | Issue | Reviewer | Confidence | Route |
|---|---|---|---|---|---|
| 2 | node.go:155-157 |
stripField error path silently returns unstripped payload. If jsonCodec.Marshal or Unmarshal fails, stripField returns the copy with the original payload (including the large field). EncodeMessage then marshals the full payload into JSON AND appends the raw bytes — the field appears in both places. Decode-side injection overwrites the JSON value with the raw value, so data correctness is preserved, but the perf optimization is silently defeated. Theoretical risk only (Marshal on known struct types won't fail), but a log warning would aid debugging. |
correctness | 60 | Advisory |
| 3 | bridge.js:76-78 |
Three separate realStdoutWrite calls for split-body output. Header, JSON body, and raw value are written in three calls. Node.js is single-threaded for user code, so no interleaving from concurrent hooks. OS pipe buffering coalesces the writes. No issue in practice. |
performance | 95 | No action |
| 4 | (missing) | No benchmarks for the perf feature. The PR claims a performance win but includes no committed benchmark. The one I ran ad-hoc showed 825µs/4MB per 800KB encode. A committed benchmark would catch regressions and validate the JSON round-trip cost. | performance | 95 | Open issue |
Coverage
- Testing gaps: Edge cases now well-covered (19 tests). No integration test exercises a full Go→Node→Go round-trip with split-body frames through the live subprocess — the bridge.js changes are tested only by code inspection, not by the test suite. This is acceptable for now since the unit-level Send/DecodeMessage tests verify the Go-side protocol parsing, and the bridge.js changes are straightforward.
- Residual risks: After any protocol parse error in
Send(), thebufio.Readerremains in an unknown state. No drain/reset mechanism exists. This is pre-existing and not introduced by this PR.
Verdict: Ready with fixes
Reasoning: All P0 and P1 findings from the first review are resolved. bridge.js now handles split-body framing in both directions. Bounds checking, negative-value rejection, and missing-header validation are all in place with new spec tests covering each case. The one remaining P2 is
stripField's JSON round-trip performance — it marshals the full 800KB payload just to build a map and delete one key. This is a measurable regression vs. the prior approach (825µs/4MB per call) that's worth fixing before merge, but it's a targeted change: restore manual map construction for the three known types as the fast path while keeping thedefaultJSON round-trip for future types.
- P2 #1: Restore per-type manual map construction in stripField for the three known payload types (avoids marshaling the 800KB field just to delete it). JSON round-trip kept as default fallback for future types. - P3 #2: Add log.Printf warnings on stripField marshal/unmarshal error paths so silent optimization skips are diagnosable. - CodeRabbit: Add mismatched split-body header validation in bridge.js (X-Body-Length present but X-Body-Field missing → error response). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Review response (re-review)All fixes pushed in d6af1c3 before these replies. P2 #1 — stripField JSON round-trip marshals 800KB fieldFixed in d6af1c3. Restored per-type manual map construction for P3 #2 — stripField error path silently returns unstripped payloadFixed in d6af1c3. Added P3 #3 — Three separate realStdoutWrite callsSkipped. Node.js is single-threaded for user code — no interleaving from concurrent hooks. OS pipe buffering coalesces the writes. P3 #4 — No benchmarksSkipped (architect issue). Benchmark specs are outside the developer scope. Already tracked in #1194 which covers benchmark and edge case test gaps. CodeRabbit nitpick — truncated split body testSkipped (architect issue). Test file modification — already tracked in #1194. CodeRabbit — bridge.js mismatched split-body headersFixed in d6af1c3. Added validation in bridge.js: |
zeroedin
left a comment
There was a problem hiding this comment.
Code Review Results (Re-review #2) ✅
Scope: Delta from 3cbd51e to d6af1c3 — internal/plugin/node.go, internal/plugin/bridge.js
Intent: Address P2/P3 feedback from the previous review round
Previous findings disposition
| Prior # | Severity | Finding | Status |
|---|---|---|---|
| P2 #1 | Moderate | stripField JSON round-trip marshals full 800KB payload |
✅ Fixed — manual map construction restored for known types; JSON round-trip kept as default fallback |
| P3 #2 | Advisory | stripField error path silently returns unstripped payload |
✅ Fixed — log.Printf warnings added for marshal/unmarshal failures |
New changes verified
-
stripFieldhybrid approach (node.go:144-192): Three known payload types (HookRenderedPayload,HookFormatRenderedPayload,HookTransformPayload) use direct map construction — avoids marshaling the large HTML/content field entirely.map[string]interface{}uses shallow copy with field exclusion.defaultcase uses JSON round-trip for future-proofing. This is the best of both worlds: fast for known types, auto-works for new types. -
bridge.jsinbound validation (bridge.js:108-112): Added check forX-Body-Lengthpresent withoutX-Body-Field— sends error response and advances buffer past the malformed frame. Matches the Go-side validation already in place. -
Log warnings on error paths (
node.go:181,186):stripFieldnow logs when marshal/unmarshal fails, making it diagnosable if the optimization is silently bypassed.
Verification
go vet ./internal/plugin/— cleango test -race -count=1 ./internal/plugin/— 308 tests pass, no races- CI — green (test + deploy preview)
- No
encoding/jsonimports in implementation or test files
Verdict: Ready ✅
Reasoning: All findings from both review rounds are resolved. The implementation correctly handles split-body framing in both Go and JavaScript, with proper bounds checking, negative-value rejection, missing-header validation, first-line stdout pollution detection, and efficient per-type field stripping. The hybrid
stripFieldapproach gives both performance (manual maps for known types) and maintainability (JSON round-trip fallback for future types). 19 spec tests cover the happy path and edge cases. CI green.
Summary
EncodeMessagefor hook-type messages withhtmlorcontentpayload fields — large strings are stripped from the JSON envelope and sent as raw bytes after it, eliminating JSON escaping overheadDecodeMessageandSendto parse multi-header frames (X-Body-Length,X-Body-Field) and inject raw body bytes back into the result mapCloses #1181
Implementation details
Outbound (
EncodeMessage):splitBodyFieldidentifies the field to split (htmlforHookRenderedPayload/HookTransformPayload,contentforHookFormatRenderedPayload,html>contentpriority formap[string]interface{}).stripFieldcreates a shallow copy of the message with the field removed from the payload (structs converted to maps to fully omit the field from JSON). The frame is:Content-Length: <N>\r\nX-Body-Length: <M>\r\nX-Body-Field: <field>\r\n\r\n<JSON><raw bytes>.Inbound (
DecodeMessage):parseFrameHeadersextracts all headers. IfX-Body-Lengthis present, reads raw bytes after the JSON body and injects them into the result map under theX-Body-Fieldname.Inbound (
Send): Header reading loop replaces single-line read, supporting arbitrary header count. After JSON body, reads split-body raw bytes ifX-Body-Lengthheader is present.Test plan
go test -race ./...passes (0 failures across all packages)EncodeMessageX-Body-Lengthis byte count, not rune counthtmltakes priority overcontentwhen both present in map payload🤖 Generated with Claude Code
Summary by CodeRabbit
html/contentoutside the JSON envelope to reduce overhead.