Last reviewed: September 3, 2026
Direct answer
A reliable CometAPI SSE stream parser must preserve three separate kinds of state: unfinished UTF-8 bytes, unfinished Server-Sent Events lines or event blocks, and unfinished application payloads such as tool-call JSON. A network read is only a transport chunk. It is not necessarily a character, line, SSE event, JSON value, or completed model response.
The correct processing order is:
response bytes
-> one persistent streaming UTF-8 decoder
-> one persistent SSE line and event parser
-> complete SSE event envelopes
-> route-specific delta accumulation
-> completion and schema checks
-> committed output
The WHATWG Server-Sent Events standard says event streams are decoded as UTF-8 and parsed line by line. A blank line dispatches an event, while an incomplete event at end-of-stream is discarded. That means splitting each network chunk on a blank line, decoding each byte chunk independently, or parsing every chunk as JSON violates the framing model.
Keep the decoder and parser alive for the lifetime of one response. Feed decoded text into the SSE parser as it arrives, but expose only events that the parser has completed. Then apply a second state machine to the event payload. A complete SSE envelope can still contain one fragment of a larger value. Anthropic’s fine-grained tool streaming documentation explicitly warns that streamed tool input can be partial or invalid JSON and describes accumulating fragments until the content block closes.
Completion must also be explicit. A clean socket close does not, by itself, prove that the application response finished. Verify the endpoint’s terminal event, stop reason, or other documented completion condition. At EOF, reject the response as truncated when any decoder bytes, partial SSE line, pending event, open content block, incomplete JSON value, or required terminal condition remains.
Happy-path operator workflow
- Validate the HTTP status and media type before reading the body. For an SSE route, require the route’s documented success status and
text/event-stream; never pass an HTML error page into the SSE parser. - Create one streaming UTF-8 decoder for the response. Preserve it across all byte reads so a multibyte character divided between reads is reconstructed correctly.
- Create one SSE parser for the response. Preserve its partial-line and partial-event buffers across reads, and enforce a configured buffer ceiling.
- Process only complete SSE events. Ignore protocol comments as comments, join repeated
datafields according to SSE rules, and keep transport chunk counts separate from event counts. - Decode the complete event envelope according to the selected route. If the event carries a partial tool-input string, append it to the correct content-block accumulator. Do not execute a tool from an open accumulator.
- When a block closes, parse and validate the accumulated value. When the response terminal condition arrives, confirm that all blocks are closed and that the stop reason is acceptable for the caller.
- Commit the final response and emit sanitized outcome telemetry. Partial text may be displayed provisionally, but it should remain distinguishable from committed output.
Error-path operator workflow
- On an unexpected status or media type, stop parsing and classify the attempt as
protocol_error. - On a fatal decoding error, malformed SSE input, or buffer-limit breach, close the response and classify the specific parser stage that failed.
- On EOF with pending state or without the required terminal condition, classify the attempt as
truncated, not successful. - If accumulated tool input is invalid, do not invoke the tool. Preserve only safe metadata and return a controlled application error.
- Decide whether to retry separately from parsing. Account for any text already shown or side effect that may already have occurred. The guidance on classifying partial success before retrying is the relevant next check.
- Reset every decoder, parser, and payload accumulator before a new attempt. Never feed a second response into state left over from the first.
Who this is for
This guide is for engineers maintaining CometAPI-backed gateways, SDK wrappers, browser clients, agent runtimes, and streaming user interfaces. It is especially relevant when code consumes a response body directly instead of delegating all framing to a mature SDK.
It also applies to on-call engineers investigating responses that end mid-sentence, tool calls that occasionally fail JSON parsing, replacement characters that appear only under load, or gateways whose memory grows during a long-lived stream. Those symptoms can share a root cause even when the upstream model is healthy: the client confused transport boundaries with protocol boundaries.
Key takeaways
- Decode bytes incrementally; do not decode every network chunk as an independent string.
- Frame SSE independently from JSON or model-delta assembly.
- Dispatch only blank-line-terminated SSE events.
- Treat a complete SSE event and a complete application payload as different milestones.
- Require a route-specific terminal condition before reporting success.
- Put explicit ceilings on partial-line, partial-event, and payload accumulators.
- Log sizes, state transitions, and outcomes rather than prompts or raw event data.
- Coordinate parser limits with the site’s streaming backpressure guide and streaming timeout guidance .
Sources checked
- The CometAPI guide to fine-grained tool streaming describes CometAPI access to incrementally streamed tool parameters and calls out incomplete JSON as a client-side integrity concern. It establishes why fragment assembly matters for this integration path.
- The WHATWG Server-Sent Events standard supplies the protocol baseline: UTF-8 decoding, line-based parsing, blank-line event dispatch, literal field handling, and rejection of an unfinished event at EOF.
- The MDN TextDecoderStream reference
documents the browser streaming decoder that converts an encoded byte stream into a string stream and can be composed with
ReadableStreamprocessing. - The maintained eventsource-parser project demonstrates the practical separation of concerns: it accepts partial or complete chunks, emits complete messages, exposes parse errors, and supports a maximum retained-buffer size.
- Anthropic’s fine-grained tool streaming contract distinguishes a complete stream event from a complete tool input. It instructs clients to concatenate partial JSON strings, parse when the block closes, guard the parse, and account for responses cut off by a token limit.
Together, these sources support a layered design. The SSE standard governs bytes-to-events behavior; decoder and parser references show implementation patterns; the provider and CometAPI material explain why application-level fragments may remain incomplete after SSE framing succeeds.
Contract details to verify
Do not hard-code one provider’s event vocabulary into a supposedly universal CometAPI adapter. Verify these details for every endpoint, model route, and SDK version you operate:
- Response gate: Confirm the expected success status and exact media type. The WHATWG processing model requires status 200 and
text/event-streamfor EventSource processing. A custom streaming client should reject an unexpected response before interpreting its body as events. - Line handling: Support CRLF, LF, and CR line endings and preserve a delimiter split across reads. Apply the SSE rule that a blank line completes an event.
- Field semantics: Confirm how
data,event,id,retry, and comment lines are used. Unknown fields and malformed retry values should have observable, bounded handling rather than silently changing application state. - Event vocabulary: Record which event denotes text deltas, tool-input deltas, content-block closure, message completion, errors, and usage. Treat those names as route-contract data, not assumptions derived from another provider.
- Payload assembly: Establish the accumulator key, such as a content-block index, and the exact close condition. A JSON fragment must remain inert until its block closes, parsing succeeds, and the resulting object passes the tool’s schema and policy checks.
- Completion: Define the terminal event or stop condition and which stop reasons count as success. A limit-related stop can leave a parameter incomplete, so transport EOF and application completion need separate flags.
- Reconnect behavior: Decide whether reconnection is supported and whether an event ID can prevent duplicate delivery. Automatic EventSource reconnection and a manually retried model request are not interchangeable.
- Resource limits: Set maximum partial-line, event, and application-accumulator sizes. The limit must be large enough for legitimate events but finite enough to stop a peer that never terminates a line or event.
- Timeouts: Measure connection establishment, time to first event, inter-event idle time, and total response time separately. A stream can be active yet never reach an application terminal condition.
A production log should make those states diagnosable without retaining content. Useful sanitized fields include:
observed_at
request_id
route
model_alias
http_status
content_type_valid
chunk_count
bytes_received
decoded_character_count
complete_sse_event_count
pending_line_character_count
pending_event_character_count
open_payload_accumulator_count
terminal_condition_seen
stop_reason
stream_outcome
buffer_limit_stage
retry_decision
duration_ms
Do not log prompts, raw data fields, tool arguments, headers, authorization material, or credentials. If removed material must be represented in an incident record, use [REDACTED]. Keep request correlation values short-lived and non-sensitive, and make the outcome one of a small controlled set such as complete, truncated, protocol_error, decode_error, payload_error, or buffer_limit.
Failure modes
Independent decoding corrupts split characters. A UTF-8 character can span byte reads. Decoding each read as a finished string can introduce replacement characters or a hard failure. A persistent streaming decoder retains the unfinished byte sequence until the next read.
Chunk splitting loses or invents SSE boundaries. A line ending or the blank line between events can be divided across reads. Splitting each chunk independently can delay an event, merge two events, or emit a partial one. The parser must retain the trailing partial line and dispatch from protocol framing, not read boundaries.
Every chunk is passed to a JSON parser. Transport chunks may contain half an envelope, several envelopes, or an envelope plus the start of another. JSON errors then appear intermittently and depend on packetization. Parse JSON only after the SSE layer emits a complete event.
Complete SSE events are mistaken for complete tool inputs. Fine-grained tool input can arrive as a sequence of strings inside otherwise valid event envelopes. Running a tool as soon as the first fragment happens to parse can execute incomplete intent. Accumulate by block, wait for closure, validate, and only then authorize execution.
Premature EOF is reported as success. A user may already see plausible text when a proxy or upstream closes the stream. Without a terminal-condition check, the client records a successful response and may cache or act on incomplete output. Keep provisional output separate and mark EOF without completion as truncated.
A partial event grows without bound. A peer can start a valid line or send repeated data lines without the blank line required for dispatch. Memory grows even though no event completes. Enforce limits at each retained-buffer layer and terminate deterministically when one is exceeded.
An error document enters the SSE parser. A gateway may return a non-stream error body. If status and media type are not checked first, the parser can produce confusing unknown-field errors while hiding the actual HTTP failure.
Retry creates duplicate work. Parser failure proves that the client lacks a trustworthy complete result; it does not prove the upstream performed no work. Before retrying a request that could trigger side effects, apply an idempotency or reconciliation rule rather than assuming failure was clean.
Raw event logging creates a second incident. Capturing payloads makes debugging easy but can retain user content or tool arguments. Prefer counts, state names, safe route identifiers, and failure positions. Enable content capture only through a separately governed diagnostic path.
FAQ
Can I split incoming text on two newline characters?
Not safely as a complete implementation. SSE recognizes CRLF, LF, and CR line endings, and either byte or character chunks can split a delimiter. Use a stateful SSE parser that implements the protocol rather than a stateless string split.
Does a valid SSE event guarantee valid JSON?
No. SSE framing only establishes that an event block ended. Its data value might not be JSON, and a valid JSON event envelope can itself carry a partial JSON string for a larger tool input. Validate at both layers.
Is a clean EOF a successful model response?
Only when the route contract says the application completed and all parser state is closed. Otherwise, EOF can leave an incomplete SSE event or payload. Track terminal-condition receipt separately from transport closure.
Should the interface display partial text?
It can, provided partial text is visibly provisional and can be withdrawn or marked incomplete. Do not cache it, trigger irreversible actions from it, or label the request successful until completion checks pass.
What should a parser test suite split?
Take valid fixtures and divide them at every byte position, including inside multibyte characters, CRLF pairs, field names, data values, blank-line delimiters, and JSON fragments. The result must be identical for every split pattern. Add malformed media types, oversized unfinished events, invalid payload JSON, missing block closure, and EOF before the terminal event.
Should the client retry every truncated stream?
No. Retry eligibility depends on request semantics, displayed output, side effects, remaining latency budget, and retry limits. The parser should report a precise outcome; a higher-level policy should decide whether another attempt is safe.
Reader next step
Build one deterministic stream fixture before changing production traffic. Run it through the exact decoder, SSE parser, and payload assembler used by your gateway. Randomize byte boundaries, force EOF at each position, inject an oversized unfinished event, and assert that only the complete fixture reaches committed state. Confirm that every failure produces sanitized fields and that no partial tool input can execute.
Then compare the parser’s memory ceiling with downstream backpressure limits and align its idle and total deadlines with the streaming timeout policy. Those checks prevent a correct parser from becoming a memory leak or an indefinitely open request.
When the fixture passes, exercise the same behavior against a non-production model route and inspect complete, truncated, payload-error, and buffer-limit outcomes before enabling the path for users.
Start with CometAPI after the parser contract and failure tests are in place.