RuntimeError: No response returned.报错分析与解决方案
前言
这是在做开源项目的时候遇到的问题,程序部署上线后不定时会突然出现这样一条报错,终于被搞烦了决定彻底查清原因。
分析
这是我正在使用的版本
fastapi==0.78.0
uvicorn==0.17.6
starlette==0.19.1
完整报错日志放在文章末尾,从报错的日志上看可以知道是在中间件上面发生的错误,报错提示RuntimeError: No response returned.
我仔细查看程序的每个函数,都可以正常返回,更换路线去查看报错提示的源代码,以下展示关键部分
async def call_next(request: Request) -> Response:
.
.
.
try:
message = await recv_stream.receive()
except anyio.EndOfStream:
if app_exc is not None:
raise app_exc
raise RuntimeError("No response returned.")
这里可以看到因为引发了anyio.EndOfStream异常,并且触发判断条件导致
我开始上网大量查阅相关信息,发现有很多人也遇到了相同的问题,经过不懈的搜查,最后在找到了可行的解决方案。
报错原因
这是由于starlette
在BaseHTTPMiddleware
中StreamingResponse
使用anyio内存对象流。
- 当你取消请求时,ASGI 应用程序会收到
"http.disconnect"
信息。 - 在你的路由函数返回后,你的最后一个中间件将
await response(...)
StreamingResponse
的async def __call__
将调用self.listen_for_disconnect
,然后调用task_group.cancel_scope.cancel()
,因为请求已经被断开了。在MemoryObjectSendStream.send
的await checkpoint()
中,在它有机会发送"http.response.start"
消息之前,流被取消检查所关闭。- 在
BaseHTTPMiddleware
的__call__
方法的这一部分中,你的倒数第二和更早的中间件会遇到anyio.EndOfStream
,同时await recv_stream.receive()
。
说大白话就是客户端取消请求的时刻,服务端有事件还在处理该请求,但请求已断开导致出现报错提示。
解决方法
现在我们已经知道报错的原因了,剩下就是解决该问题,有两种解决方法,其思路是捕获该异常并进行处理
方法1
将BaseHTTPMiddleware
子类化,以便在请求被断开的情况下忽略这个异常。
class MyBaseHTTPMiddleware(BaseHTTPMiddleware):
async def __call__(self, scope, receive, send):
try:
await super().__call__(scope, receive, send)
except RuntimeError as exc:
if str(exc) == 'No response returned.':
request = Request(scope, receive=receive)
if await request.is_disconnected():
return
raise
async def dispatch(self, request, call_next):
raise NotImplementedError()
class LoggerMiddleWare(MyBaseHTTPMiddleware):
.
.
.
方法2
因为只有最外层的BaseHTTPMiddleware
需要处理异常,所以直接实现一个SuppressNoResponseReturnedMiddleware
并把它作为你的第一个中间件。
class SuppressNoResponseReturnedMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
try:
return await call_next(request)
except RuntimeError as exc:
if str(exc) == 'No response returned.' and await request.is_disconnected():
return Response(status_code=HTTP_204_NO_CONTENT)
raise
app.add_middleware(SuppressNoResponseReturnedMiddleware)
完整报错日志
INFO: 127.0.0.1:3993 - "GET /api/v2/channel HTTP/1.1" 200 OK
ERROR: Exception in ASGI application
Traceback (most recent call last):
File "xxx\venv\lib\site-packages\anyio\streams\memory.py", line 94, in receive
return self.receive_nowait()
File "xxx\venv\lib\site-packages\anyio\streams\memory.py", line 89, in receive_nowait
raise WouldBlock
anyio.WouldBlock
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "xxx\venv\lib\site-packages\starlette\middleware\base.py", line 43, in call_next
message = await recv_stream.receive()
File "xxx\venv\lib\site-packages\anyio\streams\memory.py", line 114, in receive
raise EndOfStream
anyio.EndOfStream
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "xxx\venv\lib\site-packages\uvicorn\protocols\http\httptools_impl.py", line 375, in run_asgi
result = await app(self.scope, self.receive, self.send)
File "xxx\venv\lib\site-packages\uvicorn\middleware\proxy_headers.py", line 75, in __call__
return await self.app(scope, receive, send)
File "xxx\venv\lib\site-packages\fastapi\applications.py", line 270, in __call__
await super().__call__(scope, receive, send)
File "xxx\venv\lib\site-packages\starlette\applications.py", line 124, in __call__
await self.middleware_stack(scope, receive, send)
File "xxx\venv\lib\site-packages\starlette\middleware\errors.py", line 184, in __call__
raise exc
File "xxx\venv\lib\site-packages\starlette\middleware\errors.py", line 162, in __call__
await self.app(scope, receive, _send)
File "xxx\venv\lib\site-packages\starlette\middleware\cors.py", line 84, in __call__
await self.app(scope, receive, send)
File "xxx\venv\lib\site-packages\starlette\middleware\base.py", line 72, in __call__
response = await self.dispatch_func(request, call_next)
File "xxx\app\api\__init__.py", line 105, in mid
response = await call_next(request)
File "xxx\venv\lib\site-packages\starlette\middleware\base.py", line 47, in call_next
raise RuntimeError("No response returned.")
RuntimeError: No response returned.
参考文献
【1】https://stackoverflow.com/questions/71222144/runtimeerror-no-response-returned-in-fastapi-when-refresh-request
【2】https://github.com/encode/starlette/discussions/1527#discussioncomment-2234702
【3】https://github.com/encode/starlette/issues/1634#issuecomment-1124806406
【4】https://github.com/encode/starlette/pull/1710
【5】https://solveforum.com/forums/threads/solved-runtimeerror-no-response-returned-in-fastapi-when-refresh-request.1141379/
还有太多就不全部罗列,在此感谢所有贡献者及广大智慧。
其他
欢迎大家留言评论,一起交流心得。
这是我做的提供IPTV服务的GitHub开源项目,感兴趣的小伙伴可以看看点击跳转