Skip to content

fix: Preserve FDv2 protocol error listeners and stop misreporting server error frames - #2028

Open
joker23 wants to merge 1 commit into
mainfrom
skz/sdk-3073/common-eventsource-fdv2-error-handling
Open

joker23 wants to merge 1 commit into
mainfrom
skz/sdk-3073/common-eventsource-fdv2-error-handling

Conversation

@joker23

@joker23 joker23 commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Hardens FDv2 streaming error handling in the client-side streaming base, the server-side streaming processor, and the shared FDv2 payload stream reader. This is the first layer of the eventsource migration stack, but it is independent of the new package: the fixes are correct with the current transport and with the one that replaces it.

SSE delivers two different things under the same error event type: server-sent FDv2 event: error protocol frames, and connection-level failures. The FDv2 data sources handled that overlap in a fragile way, in two directions:

  • Protocol error frames can be lost. Both data sources registered the protocol error listener through addEventListener and then assigned onerror. An EventSource implementation may implement on* assignment by replacing the registered listeners of that type - the transport currently in use does - so the assignment silently removed the protocol listener. Server-signalled FDv2 error payloads were swallowed, and the payload processor's error path was unreachable.
  • Connection failures can be misclassified. A protocol frame also reaches the onerror slot, where it would be reported as a fabricated network error; and a connection-level event reaching PayloadStreamReader read as a protocol violation ("missing data") and closed a stream the error filter had chosen to retry.

Changes:

  • Every on* handler is assigned before the protocol listeners register, so registrations stay intact under either assignment semantics.
  • Only events that carry a string data payload are treated as protocol frames. The client onerror slot ignores them (the protocol listener owns them), and PayloadStreamReader in js-sdk-common now ignores a dataless error event instead of reporting a protocol violation - connection failures stay with the error filter, which owns retry decisions. This puts the rule in the reader itself rather than requiring every consumer to filter events before handing over the stream, and matches the semantics the client-side FDv2 listener already used. Missing data on every other event type is still reported.
  • The server processor's onclose is log-only, matching both FDv1 processors. onclose signals a deliberate close(); reporting Closed from there surfaced as a spurious Interrupted from the composite data source during normal shutdown. Terminal errors were already reported through the error filter.
  • The mock event source in the tests now models real dispatch - on* slots independent of the listener registry, slot-first ordering, and a loud failure when an event has no receiver - so a lost listener fails tests instead of passing silently. New tests cover the frame/failure distinction in the reader and on both data-source sides.

Addresses:
sdk-3162
sdk-3073


Note

Overview
Fixes FDv2 streaming where SSE error events mean two different things—server protocol frames vs connection failures—and where on* handler assignment could wipe addEventListener listeners.

PayloadStreamReader no longer treats a dataless error event as a protocol violation; missing data on other event types is still reported.

Client StreamingFDv2Base and server StreamingProcessorFDv2 now set all onopen / onerror / onclose / onretrying handlers before registering FDv2 protocol listeners, so transports that replace listeners on on* assignment keep protocol handling. Client onerror skips events whose payload has string data so server error frames are not double-reported as NETWORK_ERROR. Server onclose is log-only (no Closed status), aligning with FDv1 and avoiding spurious composite Interrupted on shutdown.

Tests upgrade mock EventSources to dispatch like real transports (independent on* slots + listener registry, slot-first) and add coverage for frame vs failure classification and listener retention.

Reviewed by Cursor Bugbot for commit 34f07d6. Bugbot is set up for automated code reviews on this repo. Configure here.

@joker23
joker23 added this pull request to stack #2035 September 18, 2026 20:32
@github-actions

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

@launchdarkly/js-sdk-common size report
This is the brotli compressed size of the ESM build.
Compressed size: 27065 bytes
Compressed size limit: 29000
Uncompressed size: 131676 bytes

@github-actions

Copy link
Copy Markdown
Contributor

@launchdarkly/js-client-sdk-common size report
This is the brotli compressed size of the ESM build.
Compressed size: 25437 bytes
Compressed size limit: 44000
Uncompressed size: 165420 bytes

@github-actions

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

@launchdarkly/js-client-sdk size report
This is the brotli compressed size of the ESM build.
Compressed size: 32627 bytes
Compressed size limit: 34000
Uncompressed size: 116865 bytes

@joker23
joker23 force-pushed the skz/sdk-3073/common-eventsource-fdv2-error-handling branch 2 times, most recently from 7ac87e5 to aa2b162 Compare September 18, 2026 21:35
@joker23
joker23 marked this pull request as ready for review September 18, 2026 22:16
@joker23
joker23 requested a review from a team as a code owner September 18, 2026 22:16

@devin-ai-integration devin-ai-integration 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.

Devin Review found 2 potential issues.

Devin Review

Comment on lines +164 to +166
if (e.headers?.[`x-ld-fd-fallback`] === `true`) {
fallbackRequested = true;
}

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.

🟡 Stale fallback survives a reconnect

When a fallback-marked connection drops before a payload, fallbackRequested remains true after an unmarked reconnect. The next payload incorrectly switches to FDv1.

Learn more

The event source reuses this processor and its onopen callback across automatic reconnections. The fallback header applies to the connection response that carries it. Assigning only on true makes the prior connection's directive sticky when a retry opens without that header. The client-side streaming base resets its pending fallback state on every open in the open handler.

Example: Connection A opens with x-ld-fd-fallback: true, then disconnects before a payload. Connection B opens without the header and sends a valid payload. That payload receives fallbackToFDv1: true, although Connection B never requested fallback.

Recommended fix: Assign fallbackRequested from the current open response on every onopen, making absent or non-true headers clear stale state.

Suggested change
if (e.headers?.[`x-ld-fd-fallback`] === `true`) {
fallbackRequested = true;
}
fallbackRequested = e.headers?.[`x-ld-fd-fallback`] === `true`;

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't think we want to do this. If the server passes a fallback directive, then we will respect it until we are able to go through the whole recovery process... even if there is a reconnect, we cannot assume that FDv2 is online whenever we reconnect the stream.

Comment thread packages/shared/sdk-server/src/data_sources/StreamingProcessorFDv2.ts Outdated
@joker23
joker23 force-pushed the skz/sdk-3073/common-eventsource-fdv2-error-handling branch from aa2b162 to 9e49a09 Compare September 21, 2026 13:43
@joker23
joker23 force-pushed the skz/sdk-3073/common-eventsource-fdv2-error-handling branch from 9e49a09 to 34f07d6 Compare September 21, 2026 14:00
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