Skip to content
AB

Consuming Server-Sent Events in Swift Without Breaking Cancellation

Parse typed SSE frames, expose AsyncSequence events, and stop network and reducer work when the user leaves.

Ashish BhakhandPublished

An AI response is not one JSON document. The server may send acknowledgement, progress, text fragments, completion, and a typed failure over one connection.

Expose that protocol as an asynchronous sequence:

Code
enum ChatEvent: Equatable {
    case acknowledgement(TurnID)
    case status(String)
    case text(String)
    case done(Usage)
    case failure(AppError)
}

func stream(_ request: TurnRequest) -> AsyncThrowingStream<ChatEvent, Error>

The parser accumulates lines until a blank line completes a frame, reads event: and data:, and decodes the payload using the schema for that event. Unknown events should be ignored or recorded safely for forward compatibility.

Cancellation is part of correctness. When a user leaves the conversation or sends a replacement request, cancel the consuming task and underlying URLSession task. The continuation's termination handler is the bridge.

Do not update SwiftUI state directly from the networking layer. Yield typed events; let the reducer decide how acknowledgement changes pending state and how text fragments update the current assistant message.

Treat an HTTP 200 as the start of the protocol, not proof of success. After headers are sent, application errors arrive as SSE error events.

Tests should feed fragmented bytes, multiple frames in one chunk, malformed JSON, unknown event names, normal completion, and cancellation. Streaming works reliably when parsing, state transitions, and lifetime are separate responsibilities.

← Back to all writing