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异常,并且触发判断条件导致

我开始上网大量查阅相关信息,发现有很多人也遇到了相同的问题,经过不懈的搜查,最后在找到了可行的解决方案

报错原因

这是由于starletteBaseHTTPMiddlewareStreamingResponse使用anyio内存对象流。

  1. 当你取消请求时,ASGI 应用程序会收到 "http.disconnect"信息。
  2. 在你的路由函数返回后,你的最后一个中间件将 await response(...)
  3. StreamingResponseasync def __call__将调用self.listen_for_disconnect,然后调用task_group.cancel_scope.cancel(),因为请求已经被断开了。在MemoryObjectSendStream.sendawait checkpoint()中,在它有机会发送 "http.response.start"消息之前,流被取消检查所关闭。
  4. 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开源项目,感兴趣的小伙伴可以看看点击跳转

posted @ 2022-10-18 00:55  Naihe\  阅读(1472)  评论(0编辑  收藏  举报
// 音乐播放器