File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change 77
88from __future__ import annotations
99
10+ import codecs
1011import json
1112from 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 :
Original file line number Diff line number Diff 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
3950async def test_parse_ignores_comments_and_other_fields () -> None :
4051 stream = _aiter ([b': keepalive\n \n event: message\n data: {"id":7}\n \n ' ])
You can’t perform that action at this time.
0 commit comments