Skip to content

EOFError in SSE streaming thread is re-raised, prevents reconnection #624

@dan98765

Description

@dan98765

Description

When the SSE streaming connection drops with an EOFError, the SDK's error handling re-raises the exception as a RuntimeError, which propagates out and kills the streaming thread. This prevents the SyncManager from receiving a PUSH_RETRYABLE_ERROR notification, so the SDK never falls back to polling or attempts to reconnect.

In our production environment, we see ~25 EOFError events per day, each generating 4 log lines (including raw puts to stdout). The connections eventually recover on a timer, but the reconnection path through push_status_handler is bypassed.

SDK Version

Tested on 8.10.1, but the same pattern exists on master.

Root Cause

In lib/splitclient-rb/sse/event_source/client.rb, the connect_stream method has nested error handling for EOFError:

# Inner rescue (around line 109)
rescue EOFError => e
  puts "SSE read operation EOF Exception!: #{e.inspect}"
  @config.logger.error("SSE read operation EOF Exception!: #{e.inspect}")
  raise 'eof exception'   # re-raises as RuntimeError

# Outer rescue (around line 130)
rescue RuntimeError
  raise 'eof exception'   # propagates out, kills the thread

The double-raise means the exception escapes the thread entirely. The SyncManager's push_status_handler never receives PUSH_RETRYABLE_ERROR, so the reconnection/polling fallback path is never triggered.

Additionally, the puts on line 110 writes directly to stdout, bypassing the configured logger.

Expected Behavior

An EOFError during SSE streaming should:

  1. Log the event through the configured logger (not puts)
  2. Notify the SyncManager via PUSH_RETRYABLE_ERROR so it can trigger reconnection or fall back to polling
  3. Not kill the streaming thread

I see PR #622 touches some of the reconnection logic, but the specific double-raise pattern and puts call remain.

Suggested Fix

Replace the re-raise with a push_status notification, similar to how other connection errors are handled:

rescue EOFError => e
  @config.logger.error("SSE EOF: #{e.inspect}")
  @on[:action].call(Splits::EventSource::Actions::PUSH_RETRYABLE_ERROR)

Happy to submit a PR for either or both of these if helpful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions