Skip to content

fix(notification-client): guard JSON parsing of notification frames#232

Merged
jbiskur merged 1 commit into
mainfrom
fix/notification-client-guard-parse
Jun 23, 2026
Merged

fix(notification-client): guard JSON parsing of notification frames#232
jbiskur merged 1 commit into
mainfrom
fix/notification-client-guard-parse

Conversation

@jbiskur

@jbiskur jbiskur commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Problem

NotificationClient's WebSocket onmessage handler did unguarded JSON.parse(parsedData) then JSON.parse(data.message). Because it runs inside the onmessage callback, a single malformed frame (non-JSON, empty/absent message, truncated payload, or a non-event control frame) threw an uncaught error that crashed the host process.

Observed repeatedly in production (fishfacts-ai-backend): intermittent pod crashes (exit 1) every few hours, and when the crashing pod was the data-pump leader it wedged the pump cluster-wide until a manual rolling restart. Captured real frames confirm the normal shape is {"message":"<stringified-JSON event>"} (no type), so any frame whose message isn't valid JSON hits the unguarded parse.

Fix

Extract the frame handling into handleMessage() and guard every parse — a bad frame is logged (warn) and skipped instead of throwing. No behaviour change for valid frames.

Tests (TDD)

Added test/tests/common/notification-client.test.ts: written first, went red on the malformed cases (reproducing the crash), then green after the guard. Covers: valid frame emits correctly, malformed/empty/missing message, missing inner data, non-JSON outer frame, and validation frames. Full suite 210/210, build clean.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced notification service reliability by implementing graceful handling of malformed WebSocket messages that previously caused disconnections.
    • Added robust validation and error logging for invalid notification frames.
  • Tests

    • Introduced comprehensive test suite covering notification message parsing, including malformed input and edge cases.

The WebSocket onmessage handler did an unguarded JSON.parse(parsedData) and
JSON.parse(data.message). It runs inside the onmessage callback, so a single
malformed frame (non-JSON, empty/absent `message`, truncated payload, or a
non-event control frame) threw an uncaught error and crashed the host process
— observed repeatedly in production, where it also wedged the data-pump.

Extract the frame handling into handleMessage() and guard every parse: a bad
frame is logged (warn) and skipped instead of throwing. Adds regression tests
covering valid frames, malformed/empty/missing `message`, missing inner `data`,
non-JSON outer frames, and validation frames.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The NotificationClient WebSocket onmessage handler is refactored to delegate to a new private handleMessage(rawData: string) method. This method adds two-level guarded JSON parsing, skips validation-type control frames, validates payload shape, maps fields to NotificationEvent, and enforces maxEvents. A new Bun test file covers all valid and malformed input cases.

Changes

NotificationClient handleMessage refactor and tests

Layer / File(s) Summary
handleMessage implementation and onmessage wiring
src/common/notification-client.ts
onmessage now calls a new private handleMessage(parsedData). The method wraps two JSON.parse calls in try/catch, returns early on validation-type control frames, checks for a data field before emitting, maps fields (aggregatorflowType, etc.) to observer.next, and calls observer.complete() plus WebSocket close when maxEvents is reached.
handleMessage test suite
test/tests/common/notification-client.test.ts
Adds recordingLogger() and makeClient() test helpers, a validFrame() factory, and tests for: well-formed frame emission and field mapping; malformed inner JSON; empty/missing message; missing data field; non-JSON outer payload; and validation-type control frames triggering logger errors without emitting events.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 Hop hop, the frames come fast,
But bad JSON won't slip on past!
Two try/catch walls stand firm and true,
Validation frames get skipped right through.
The rabbit checks each data field,
And only good events are revealed! 🌿

🚥 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding JSON parsing guards to the NotificationClient to prevent crashes from malformed notification frames.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/notification-client-guard-parse

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.

🧹 Nitpick comments (2)
src/common/notification-client.ts (1)

277-277: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Type assertion could be more defensive.

The type assertion as string on data.message assumes the value is a string, but data.message is typed as string | undefined. While the try-catch will handle JSON.parse errors from undefined or other non-string values, the error message at lines 283-285 may be misleading (e.g., if data.message is undefined, it will show type undefined in the log).

Consider adding an explicit check before parsing:

🛡️ More defensive guard suggestion
+    if (!data.message || typeof data.message !== "string") {
+      this.logger.warn(
+        `Discarding notification frame: 'message' field is missing or not a string (type ${typeof data.message})`
+      )
+      return
+    }
+
     let parsed: { pattern: string; data: NotificationEventData }
     try {
-      parsed = JSON.parse(data.message as string) as {
+      parsed = JSON.parse(data.message) as {
         pattern: string
         data: NotificationEventData
       }
🤖 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 `@src/common/notification-client.ts` at line 277, The type assertion `as
string` on `data.message` in the JSON.parse call is not defensive since
`data.message` is typed as `string | undefined`, which can cause misleading
error messages. Before the JSON.parse call around line 277, add an explicit
check to verify that `data.message` is actually a string (e.g., using typeof or
checking for undefined), and handle the case where it is not a string with an
appropriate error before attempting to parse. This ensures the error logging at
lines 283-285 will provide accurate type information and clearer context about
what went wrong.
test/tests/common/notification-client.test.ts (1)

49-94: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding test coverage for maxEvents enforcement.

The test suite thoroughly covers the guarded parsing and error handling paths, but doesn't verify the maxEvents enforcement logic (lines 311-315 in the implementation). While this is existing functionality that was moved into the new method, adding a test would improve coverage of all code paths in handleMessage.

🧪 Example test to add
it("completes observer and closes connection when maxEvents is reached", () => {
  const observer = new Subject<NotificationEvent>()
  let completed = false
  observer.subscribe({
    complete: () => { completed = true }
  })
  const logger = recordingLogger()
  const client = new NotificationClient(
    observer,
    { apiKey: "test-key" },
    { tenant: "t", dataCore: "dc" },
    { logger, maxEvents: 2 }
  )
  const handle = (raw: string) => (client as unknown as { handleMessage(raw: string): void }).handleMessage(raw)
  
  handle(validFrame())
  handle(validFrame())
  
  assertEquals(completed, true)
})

Note: This test would also need to verify that webSocket.close() was called, which might require mocking the WebSocket instance.

🤖 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 `@test/tests/common/notification-client.test.ts` around lines 49 - 94, Add a
new test case to the NotificationClient.handleMessage test suite that verifies
the maxEvents enforcement logic. Create a test that instantiates a
NotificationClient with a low maxEvents value (e.g., 2), calls the handleMessage
method that many times with valid frames, and then asserts that the observer's
complete callback was invoked and the underlying webSocket.close() method was
called to verify the connection was properly closed when the event limit was
reached.
🤖 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 `@src/common/notification-client.ts`:
- Line 277: The type assertion `as string` on `data.message` in the JSON.parse
call is not defensive since `data.message` is typed as `string | undefined`,
which can cause misleading error messages. Before the JSON.parse call around
line 277, add an explicit check to verify that `data.message` is actually a
string (e.g., using typeof or checking for undefined), and handle the case where
it is not a string with an appropriate error before attempting to parse. This
ensures the error logging at lines 283-285 will provide accurate type
information and clearer context about what went wrong.

In `@test/tests/common/notification-client.test.ts`:
- Around line 49-94: Add a new test case to the NotificationClient.handleMessage
test suite that verifies the maxEvents enforcement logic. Create a test that
instantiates a NotificationClient with a low maxEvents value (e.g., 2), calls
the handleMessage method that many times with valid frames, and then asserts
that the observer's complete callback was invoked and the underlying
webSocket.close() method was called to verify the connection was properly closed
when the event limit was reached.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 36f58423-dd71-41fd-a395-2e52a901af9e

📥 Commits

Reviewing files that changed from the base of the PR and between bd2dbfd and d2b8488.

📒 Files selected for processing (2)
  • src/common/notification-client.ts
  • test/tests/common/notification-client.test.ts

@jbiskur jbiskur merged commit 2b84c0e into main Jun 23, 2026
2 checks passed
@jbiskur jbiskur deleted the fix/notification-client-guard-parse branch June 23, 2026 11:36
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