[Bug]: SSE client splits data lines on U+2028, U+2029 and U+0085, so streamed events fail to parse

Offen
#1,432 0 Kommentare 0 Reaktionen 2 zugewiesene Personen Auf GitHub ansehen

Maintainer antworten meist innerhalb von 2 Tagen

@rohityan arbeitet bereits daran.

Seit 07.11.2026.

  • #1433 von @devinhuang2001 — offen

Bewertung

Dieses Issue wurde noch nicht bewertet.

Beschreibung

What happened?

parse_sse_stream in src/a2a/client/transports/http_helpers.py reads the stream with response.aiter_lines() (line 78). httpx's line decoder follows str.splitlines(), so it also ends a line at U+2028 (LINE SEPARATOR), U+2029 (PARAGRAPH SEPARATOR) and U+0085 (NEL). SSE line ends are only CR, LF and CRLF, and the docstring on line 73 says the parser conforms to the SSE spec.

The server writes these three characters raw: json_utils.dumps uses ensure_ascii=False (src/a2a/utils/json_utils.py:19). sse-starlette frames on CR/LF only, so it sends one data: line. The client splits that line into several. json.loads fails on the first piece, and the stream ends with A2AClientError('Invalid JSON-RPC response in SSE stream').

Any string field in a streamed event can hold these characters, for example message text, artifacts or status messages that quote a document. Resubscribing sends the same task snapshot, so that task stays unreadable over streaming. Non-streaming GetTask is not affected. The same parser is used by the JSON-RPC and REST transports and by the v0.3 compat transports.

This looks like a regression from #1132, which removed httpx-sse. httpx-sse 0.4.3 split SSE lines on CR/LF only (_splitlines_sse).

Repro (library level, using the versions pinned in uv.lock: sse-starlette 3.4.4, httpx 0.28.1)

import asyncio, json
import httpx
from sse_starlette.sse import ServerSentEvent

async def main():
    data = json.dumps({"text": "before
after"}, ensure_ascii=False)
    frame = ServerSentEvent(data=data).encode()
    print(frame.count(b"data:"))  # 1: the server sends one data line
    async def body():
        yield frame
    resp = httpx.Response(200, content=body())
    lines = [l async for l in resp.aiter_lines()]
    print(len(lines))             # 3: the client sees the data line split in two
    json.loads(lines[0][len("data: "):])

asyncio.run(main())

Expected: one data: line that parses as JSON. Observed: JSONDecodeError.

Possible fixes:

  • Split SSE lines on CR, LF and CRLF only, with a small line decoder over aiter_text().
  • Also, or instead: escape U+2028, U+2029 and U+0085 in json_utils.dumps.

Related: #1213, the end-of-stream flush in the same parser.

No existing tests cover these characters in SSE data.

Relevant log output
json.decoder.JSONDecodeError: Unterminated string starting at: line 1 column 10 (char 9)
Code of Conduct
  • I agree to follow this project's Code of Conduct

Drafted with Claude Code. I reviewed the code paths and ran the repro.

Vorherrschende Sprache
Python
Sterne
2.2k
Forks
532
Ø Merge
3 T. 2 Std.
Gemergte PRs (30 T.)
46

Entwicklungsumgebung

Erste Schritte

  1. Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
  3. Forken Sie das Repository und arbeiten Sie in einem Branch.
  4. Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.

Mehr aus a2aproject/a2a-python

Alle Issues in a2aproject/a2a-python

Ähnliche Issues

Weitere Issues zu Python

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.