Skip to content

Commit 855a8a4

Browse files
kasarolzzwCoda-bot
andcommitted
fix: [Coda] 修复流式请求中上下文管理器使用错误导致的AttributeError
(LogID: 202509222042560100911040167333F06) Co-Authored-By: Coda <coda@bytedance.com>
1 parent bee46dc commit 855a8a4

4 files changed

Lines changed: 53 additions & 15 deletions

File tree

‎cozeloop/internal/httpclient/http_client.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ def __init__(self):
2626
def request(self, method: str, url: URL | str, **kwargs: Any) -> Response:
2727
return self.sync_client.request(method, url, **kwargs)
2828

29-
def stream(self, method: str, url: URL | str, **kwargs: Any) -> typing.Iterator[Response]:
30-
yield self.sync_client.stream(method, url, **kwargs)
29+
def stream(self, method: str, url: URL | str, **kwargs: Any):
30+
"""返回同步流上下文管理器"""
31+
return self.sync_client.stream(method, url, **kwargs)
3132

3233
async def arequest(self, method: str, url: URL | str, **kwargs: Any) -> Response:
3334
return await self.async_client.request(method, url, **kwargs)

‎cozeloop/internal/prompt/execute_stream_reader.py‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
128128
async def __aenter__(self):
129129
"""异步上下文管理器入口"""
130130
if not self._context_entered:
131-
self._response = self._stream_context.__enter__() # 检查HTTP状态码(同步版本逻辑)
131+
self._response = await self._stream_context.__aenter__() # 检查HTTP状态码(异步版本逻辑)
132132
if self._response.status_code != 200:
133133
try:
134134
# 先读取完整响应内容
@@ -139,10 +139,10 @@ async def __aenter__(self):
139139
log_id = self._response.headers.get("x-tt-logid", "")
140140
error_code = error_data.get('code', 0)
141141
error_msg = error_data.get('msg', 'Unknown error')
142-
self._stream_context.__exit__(None, None, None)
142+
await self._stream_context.__aexit__(None, None, None)
143143
raise RemoteServiceError(self._response.status_code, error_code, error_msg, log_id)
144144
except Exception as e:
145-
self._stream_context.__exit__(None, None, None)
145+
await self._stream_context.__aexit__(None, None, None)
146146
if isinstance(e, RemoteServiceError):
147147
raise
148148
from cozeloop.internal.consts.error import InternalError
@@ -158,7 +158,7 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
158158
"""异步上下文管理器出口"""
159159
await self.aclose()
160160
if self._context_entered:
161-
return self._stream_context.__exit__(exc_type, exc_val, exc_tb)
161+
return await self._stream_context.__aexit__(exc_type, exc_val, exc_tb)
162162

163163
def __iter__(self):
164164
"""支持for循环直接读取"""
@@ -206,9 +206,9 @@ async def aclose(self) -> None:
206206
self._closed = True
207207
# 如果还没有进入上下文,直接关闭stream_context
208208
if not self._context_entered:
209-
if hasattr(self._stream_context, '__exit__'):
209+
if hasattr(self._stream_context, '__aexit__'):
210210
try:
211-
self._stream_context.__exit__(None, None, None)
211+
await self._stream_context.__aexit__(None, None, None)
212212
except Exception:
213213
pass
214214
return
@@ -218,8 +218,8 @@ async def aclose(self) -> None:
218218
await super().aclose()
219219
else:
220220
# 如果response属性不存在,只关闭stream_context
221-
if hasattr(self._stream_context, '__exit__'):
221+
if hasattr(self._stream_context, '__aexit__'):
222222
try:
223-
self._stream_context.__exit__(None, None, None)
223+
await self._stream_context.__aexit__(None, None, None)
224224
except Exception:
225225
pass

‎cozeloop/internal/stream/base_stream_reader.py‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,8 @@ async def _aiter_events(self) -> AsyncIterator[ServerSentEvent]:
7878
ServerSentEvent: 解码后的SSE事件
7979
"""
8080
try:
81-
# 由于httpx.stream()返回的是同步流,即使在异步上下文中也需要使用同步迭代
82-
# 将同步迭代包装成异步生成器
83-
for sse in self._decoder.iter_bytes(self.response.iter_bytes()):
81+
# 使用异步迭代器
82+
async for sse in self._decoder.aiter_bytes(self.response.aiter_bytes()):
8483
yield sse
8584
except Exception as e:
8685
logger.error(f"Error async iterating SSE events: {e}")

‎cozeloop/internal/stream/sse.py‎

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from __future__ import annotations
55

66
import json
7-
from typing import Any, Iterator, Optional
7+
from typing import Any, Iterator, Optional, AsyncIterator
88

99

1010
class ServerSentEvent:
@@ -129,6 +129,44 @@ def _iter_chunks(self, iterator: Iterator[bytes]) -> Iterator[bytes]:
129129
if data:
130130
yield data
131131

132+
async def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]:
133+
"""
134+
异步解码字节流为SSE事件
135+
136+
Args:
137+
iterator: 异步字节流迭代器
138+
139+
Yields:
140+
ServerSentEvent: 解码后的SSE事件
141+
"""
142+
async for chunk in self._aiter_chunks(iterator):
143+
# 先分割再解码,确保splitlines()只使用\r和\n
144+
for raw_line in chunk.splitlines():
145+
line = raw_line.decode("utf-8")
146+
sse = self.decode(line)
147+
if sse:
148+
yield sse
149+
150+
async def _aiter_chunks(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[bytes]:
151+
"""
152+
异步处理字节块,确保完整的SSE消息
153+
154+
Args:
155+
iterator: 异步字节流迭代器
156+
157+
Yields:
158+
bytes: 完整的SSE消息块
159+
"""
160+
data = b""
161+
async for chunk in iterator:
162+
for line in chunk.splitlines(keepends=True):
163+
data += line
164+
if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")):
165+
yield data
166+
data = b""
167+
if data:
168+
yield data
169+
132170
def decode(self, line: str) -> Optional[ServerSentEvent]:
133171
"""
134172
解码单行SSE数据
@@ -182,4 +220,4 @@ def decode(self, line: str) -> Optional[ServerSentEvent]:
182220
pass
183221
# 其他字段被忽略
184222

185-
return None
223+
return None

0 commit comments

Comments
 (0)