Skip to content

Commit ce23c4a

Browse files
fix: handle UTF-8 split across SSE chunks (#135)
1 parent e668ed9 commit ce23c4a

2 files changed

Lines changed: 16 additions & 1 deletion

File tree

src/acp/_sse.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from __future__ import annotations
99

10+
import codecs
1011
import json
1112
from typing import TYPE_CHECKING, Any
1213

@@ -62,11 +63,12 @@ async def parse_sse_stream(chunks: AsyncIterator[bytes]) -> AsyncIterator[dict[s
6263
Multi-line ``data:`` fields are concatenated with newlines per the spec. A
6364
blank line dispatches the buffered event.
6465
"""
66+
decoder = codecs.getincrementaldecoder("utf-8")()
6567
buffer = ""
6668
data_lines: list[str] = []
6769

6870
async for chunk in chunks:
69-
buffer += chunk.decode("utf-8")
71+
buffer += decoder.decode(chunk)
7072
while "\n" in buffer:
7173
line, buffer = buffer.split("\n", 1)
7274
line = line.rstrip("\r")
@@ -78,6 +80,8 @@ async def parse_sse_stream(chunks: AsyncIterator[bytes]) -> AsyncIterator[dict[s
7880
elif not line.startswith(":"):
7981
_append_field(line, data_lines)
8082

83+
decoder.decode(b"", final=True)
84+
8185
# Flush a trailing event with no terminating blank line.
8286
event = _decode_event(data_lines)
8387
if event is not None:

tests/http/test_sse.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ async def test_parse_multiple_events_split_across_chunks() -> None:
3535
assert events == [{"id": 1}, {"id": 2}]
3636

3737

38+
@pytest.mark.asyncio
39+
async def test_parse_multibyte_utf8_split_across_chunks() -> None:
40+
frame = 'data: {"text":"你好"}\n\n'.encode()
41+
split_at = frame.index("你".encode()) + 1
42+
stream = _aiter([frame[:split_at], frame[split_at:]])
43+
44+
events = [event async for event in parse_sse_stream(stream)]
45+
46+
assert events == [{"text": "你好"}]
47+
48+
3849
@pytest.mark.asyncio
3950
async def test_parse_ignores_comments_and_other_fields() -> None:
4051
stream = _aiter([b': keepalive\n\nevent: message\ndata: {"id":7}\n\n'])

0 commit comments

Comments
 (0)