Skip to content

Overhaul parser for spec compliance; add reconnecting EventSource client#1

Merged
zac merged 6 commits into
mainfrom
sse-library-gaps
Jul 10, 2026
Merged

Overhaul parser for spec compliance; add reconnecting EventSource client#1
zac merged 6 commits into
mainfrom
sse-library-gaps

Conversation

@zac

@zac zac commented Jul 10, 2026

Copy link
Copy Markdown
Member

Summary

Closes the functional gaps between this library and the WHATWG server-sent events specification, and adds the missing pieces needed to use it as a complete SSE client.

Critical fixes

  • Errors no longer hang the consumer. The parser previously swallowed transport errors with a print and never finished its stream, leaving for await suspended forever on any network drop. Parsing is now iterator-driven and rethrows transport errors to the consumer.
  • Cancellation propagates. There is no detached parse task or unbounded AsyncStream buffer anymore; bytes are read only as events are consumed, and cancelling the consuming task stops the parse and releases the connection.
  • Invalid UTF-8 can no longer fabricate events. Undecodable bytes previously turned a line into "", which the parser treated as a dispatch boundary — splitting one event in two and dropping data. Decoding now uses U+FFFD replacement per spec.

Spec compliance

  • A single leading BOM is stripped.
  • Fixed the line splitter emitting a phantom empty line for CR CR sequences.
  • The last event ID persists across events and is stamped on every dispatched event (lastEventId), commits at dispatch time (so a truncated final block can't poison the resume ID), and id-only blocks commit without dispatching — all per spec.
  • ServerSentEvent.type applies the spec's "message" default when the event field is absent or empty.
  • The URLSession helpers now send Accept: text/event-stream and Cache-Control: no-cache, and validate the response (status 200, text/event-stream content type), throwing SSEError instead of parsing an error page as a stream.

New: EventSource

A reconnecting client mirroring the spec's EventSource model:

  • automatic reconnection when the server closes the stream or the network drops
  • honors the server's retry interval
  • resumes with the Last-Event-ID header
  • HTTP 204 ends the sequence (the spec's "stop reconnecting" signal); other non-200 responses fail the connection with a thrown error
for try await event in EventSource(url: url) {
    print(event.type, event.data)
}

Structure & cleanup

  • The parser is now generic over any AsyncSequence of bytes, decoupling it from URLSession.AsyncBytes — it builds and tests on Linux (the URLSession pieces are Darwin-gated), and CI gains a Linux job.
  • Removed dead API: the never-populated Event.comment field, the unused trim() helper, and the unused AsyncSequence.split extensions.
  • README rewritten (the previous example called a nonexistent asyncBytes(from:) API) and the feature checklist updated.
  • ~15 new tests covering BOM handling, invalid UTF-8, error propagation, last-event-ID persistence and dispatch-time commit, type defaulting, cancellation, and request/response validation.

Breaking changes

  • AsyncServerSentEvents.Event is now the top-level ServerSentEvent (a typealias keeps the old spelling reachable); the comment property is gone.
  • AsyncServerSentEvents is generic over its byte source and its iteration can now throw, so consumers must use for try await.
  • serverSentEvents(from:)/(for:) now throw SSEError for non-200 or non-text/event-stream HTTP responses.

Test plan

  • Full suite passes on macOS (Swift 6.0) and the new Linux job (Swift 6.0), including the pre-existing parsing/resilience suites and the new spec-compliance suite.

Generated by Claude Code

claude added 2 commits July 10, 2026 14:13
Rework the parser to be iterator-driven and generic over any
AsyncSequence of bytes:

- Errors from the byte stream are rethrown to the consumer instead of
  being swallowed (previously a network failure left the stream open
  and the consumer awaiting forever)
- Cancellation propagates to the transport; no more detached parse task
  buffering events without bound
- UTF-8 decodes with replacement characters, so invalid bytes no longer
  fabricate blank lines that split events
- A single leading BOM is stripped per spec
- Fixed the line splitter emitting a phantom empty line for CR CR
- The last event ID persists across events, is stamped on every
  dispatched event, and commits at dispatch time so an incomplete
  final block cannot poison the resume ID
- ServerSentEvent.type applies the spec's "message" default
- Removed the never-populated comment field and unused split helpers

URLSession helpers now send Accept: text/event-stream and
Cache-Control: no-cache, and validate the response status code and
content type, throwing SSEError otherwise.

New EventSource client mirrors the spec's reconnection model:
automatic reconnect on close or network error, retry-interval
handling, Last-Event-ID resumption, and HTTP 204 to stop.

The parser now builds on Linux (URLSession pieces are Darwin-gated);
CI gains a Linux job.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NkByVdGaxGYiRiGnaQJ8Uc
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NkByVdGaxGYiRiGnaQJ8Uc

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8971b446b1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/AsyncServerSentEvents/URLSession+AsyncServerSentEvents.swift Outdated
Comment thread Sources/AsyncServerSentEvents/URLSession+AsyncServerSentEvents.swift Outdated
claude added 4 commits July 10, 2026 15:20
The content-type check now requires the media type to be exactly
text/event-stream (parameters allowed), rejecting lookalikes such as
text/event-stream+json. An explicitly cleared last event ID (empty id:
field) now removes a caller-set Last-Event-ID header on reconnect
instead of resuming from the stale value.
URLSession.AsyncBytes is unavailable on Linux, which kept the URLSession
helpers and EventSource Apple-only. SSEByteStream streams a data task's
body through URLSessionDataDelegate — available on both Darwin and
corelibs Foundation — so the entire library now works on Linux.

Connections run on a session derived from the caller's session
configuration (a session-level delegate is required for streaming);
dropping the stream cancels the task and invalidates the session. The
delegate-taking serverSentEvents variants are removed since per-task
delegates don't compose with the streaming delegate.

Header lookup goes through allHeaderFields for consistent behavior on
corelibs Foundation, and new tests cover chunk-boundary parsing and
error propagation through the byte stream.
corelibs URLSession does not support file: URLs for data tasks, so the
streaming tests now run against a minimal scripted HTTP server that
works on both Darwin and Linux. This also enables true end-to-end
coverage: request headers are asserted on the wire, and a new
EventSource test drives the full reconnect cycle (Last-Event-ID on
reconnect, retry interval pickup, 204 termination).

The byte-to-line state machine moves to a top-level SSELineIterator so
tests can drive it directly, with a dedicated suite covering CR/LF/CRLF
equivalence, empty lines, consecutive CRs, trailing terminators at end
of stream, CRLF split across chunk boundaries, multi-byte characters
split across chunks, and that NEL/U+2028/U+2029 are content, not
boundaries.
@zac zac merged commit 20ed69a into main Jul 10, 2026
4 checks passed
@zac zac deleted the sse-library-gaps branch July 10, 2026 17:52
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.

2 participants