Gunicorn: 老错误重现:'Response'对象在带有websockets的wsgi.py中没有属性'status_code'

创建于 2018-08-09  ·  33评论  ·  资料来源: benoitc/gunicorn

就像这个老issue 1210说的,gunicorn 在客户端断开连接时记录错误,我的环境是:

  • Debian GNU/Linux 7.8

  • nginx

  • Python3.4

  • gunicorn(19.8.1)(有一名或多名工人)

  • Flask-SocketIO,客户端指定websocket传输

一切正常,包括客户端,除了这个错误日志,两个独立于云的生产实例都持久记录,但我无法在我的开发机器上重现它,这是一个 mac。

非常感谢您的帮助。

错误处理请求 /socket.io/?EIO=3&transport=websocket
回溯(最近一次通话最后):
文件“/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/workers/async.py”,第 56 行,在句柄中
self.handle_request(listener_name, req, client, addr)
文件“/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/workers/async.py”,第 116 行,在 handle_request
resp.close()
文件“/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py”,第 409 行,关闭
self.send_headers()
文件“/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py”,第 325 行,在 send_headers
tosend = self.default_headers()
文件“/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py”,第 306 行,在 default_headers
elif self.should_close():
文件“/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py”,第 229 行,在 should_close
如果 self.status_code < 200 或 self.status_code 在 (204, 304):
AttributeError:“响应”对象没有属性“状态代码”

Feedback Requested unconfirmed ThirdPartFlask

最有用的评论

撞@benoitc

所有33条评论

你有任何简单的例子来重现它吗? 如果可能,也请尝试使用最新的大师。

之前我在本地开发环境试了好几次,都是和生产环境一样的应用代码,但是无法重现。

并且查看了19.9.0版本的发布日志,没有找到相关的,我会保留
看这个错误日志,如果发现新的东西,我会在这里发帖。

我也有这个问题,特别是当我强制所有从客户端到 websocket 协议的连接时。 我的设置和 BoWuGit 一样。 如果在升级之前允许轮询协议,这不会出现,而是另一个错误:
`
[错误] 错误处理请求 /socket.io/?EIO=3&transport=polling&t=MPRHuoV&sid=cd64be7c940e474d8728b114c3fb9bbe

回溯(最近一次通话最后):
文件“/usr/local/lib/python3.6/site-packages/gunicorn/workers/async.py”,第 56 行,在句柄中
self.handle_request(listener_name, req, client, addr)

文件“/usr/local/lib/python3.6/site-packages/gunicorn/workers/async.py”,第 107 行,在 handle_request
respiter = self.wsgi(环境,resp.start_response)

__call__ 中的文件“/usr/local/lib/python3.6/site-packages/flask/app.py”,第 1994 行
return self.wsgi_app(environ, start_response)

__call__ 中的文件“/usr/local/lib/python3.6/site-packages/flask_socketio/__init__.py”,第 43 行
开始响应)

文件“/usr/local/lib/python3.6/site-packages/engineio/middleware.
py",第 47 行,在 __call__ 中
return self.engineio_app.handle_request(environ, start_response)

文件“/usr/local/lib/python3.6/site-packages/socketio/server.py”,第 360 行,在 handle_request
返回 self.eio.handle_request(环境,start_response)

文件“/usr/local/lib/python3.6/site-packages/engineio/server.py”,第 279 行,在 handle_request
套接字 = self._get_socket(sid)

_get_socket 中的文件“/usr/local/lib/python3.6/site-packages/engineio/server.py”,第 439 行
raise KeyError('会话已断开')
`
但是我怀疑这可能与彼此有关,因为我强制连接为websocket,因此再也没有出现此错误。

在使用 eventlet 0.24.1 时,gunicorn 19.9.0 和 Flask-socketIO 3.0.2 也有这个问题

AttributeError:“响应”对象没有属性“状态代码”

也遇到此问题,满足以下要求:

Flask==1.0.2
gunicorn==19.5.0
python-socketio==2.0.0
eventlet==0.24.1

关闭已打开套接字连接的 Web 浏览器时出现错误消息:

 Error handling request /socket.io/?EIO=3&transport=websocket&sid=d43ec0ae0bb946debc51f1ca2e5b8a94
Traceback (most recent call last):
  File "/usr/lib/python2.7/dist-packages/gunicorn/workers/async.py", line 52, in handle
    self.handle_request(listener_name, req, client, addr)
  File "/usr/lib/python2.7/dist-packages/gunicorn/workers/async.py", line 114, in handle_request
    resp.close()
  File "/usr/lib/python2.7/dist-packages/gunicorn/http/wsgi.py", line 403, in close
    self.send_headers()
  File "/usr/lib/python2.7/dist-packages/gunicorn/http/wsgi.py", line 319, in send_headers
    tosend = self.default_headers()
  File "/usr/lib/python2.7/dist-packages/gunicorn/http/wsgi.py", line 300, in default_headers
    elif self.should_close():
  File "/usr/lib/python2.7/dist-packages/gunicorn/http/wsgi.py", line 233, in should_close
    if self.status_code < 200 or self.status_code in (204, 304):
AttributeError: 'Response' object has no attribute 'status_code'

看起来这个问题已在最新版本的python-engineio中得到修复。

用python-engineio最新版本(2.3.2)测试过,还是不行。

关于这个问题的任何消息? 使用 sentry-python 时出现同样的错误

我有同样的问题

小事件:0.25.1
烧瓶socketio:4.2.1
独角兽:19.9.0

image

image

如何重现它? 你提供一个简单的例子吗?

我也不确定如何重现它,但是当我在我的 gunicorn 应用程序上刷新页面时,它经常发生

遇到同样的问题,我的环境和@eazow一样,而gunicorn == 20.0.4。
看来问题是在我安装了哨兵进行错误跟踪之后发生的。
这些问题可以通过以下方式重现

  1. 刷新页面(不打开新页面)
  2. 关闭页面

有趣的是,打开新页面不会产生问题。 不知道为什么。 谢谢!

我和@cowbonlin有同样的问题。 同样的gunicorn版本。

安装哨兵后,我们收到了大量的此错误。 虽然我发现很难判断这是否总是发生 - 因为我们没有在哨兵之前跟踪错误。

虽然它似乎不会影响我们服务器的实际功能,但这只是大量的垃圾邮件。

我们正在经历同样的事情。 Sentry 已安装但已禁用。 有任何想法吗?

安装哨兵的问题相同。

您是否有任何示例可以在没有哨兵的情况下重现它(禁用与否)?

此外,我手动点击 /api 而不是命名空间。

此外,我手动点击 /api 而不是命名空间。

这是什么意思 ? 这个哨兵有关系吗?

此外,我手动点击 /api 而不是命名空间。

这是什么意思 ? 这个哨兵有关系吗?

不,这与 socket.io 命名空间有关。 我尝试删除它们,即使在删除它们之后,我也会收到错误消息。 但是,我在没有 gunicorn 或 nginx 的本地机器上遇到了这个其他错误,这可能是相关的。

这些是我的要求:

sentry_sdk == 0.14.3
Flask_SocketIO == 4.2.1
eventlet == 0.25.1

这是我在服务器端的 flask-socketio 代码:

socketio = SocketIO(engineio_logger=True, logger=True, debug=True, cors_allowed_origins="*", path='/socket.io')
...
socketio.init_app(app, async_mode="eventlet")

这是我在客户端的 React 套接字 io 代码:

          this.socket = io.connect(`http://localhost:5000?info=${someInfo}`, {
            transports: ['websocket', 'polling'] // an attempt to keep polling as a fallback but start on websockets
          });

让我知道这是否有帮助。 在 Ubuntu 上,错误看起来像上面的错误,而在 Windows 本地,它看起来像这样:
```Traceback(最近一次调用最后):
文件“C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py”,第 599 行,在 handle_one_response
写(b'')
文件“C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py”,第 491 行,写入
在 start_response() 之前引发 AssertionError("write()")
AssertionError:在 start_response() 之前写入()

在处理上述异常的过程中,又出现了一个异常:

回溯(最近一次通话最后):
文件“C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py”,第 357 行,在 __init__
self.handle()
文件“C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py”,第 390 行,在句柄中
self.handle_one_request()
文件“C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py”,第 466 行,在 handle_one_request
self.handle_one_response()
文件“C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py”,第 609 行,在 handle_one_response
写(err_body)
文件“C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py”,第 538 行,写入
wfile.flush()
文件“C:\ProgramData\Anaconda3\lib\socket.py”,第 607 行,写入
返回 self._sock.send(b)
文件“C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenio\base.py”,第 397 行,在发送中
return self._send_loop(self.fd.send, data, flags)
_send_loop 中的文件“C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenio\base.py”,第 384 行
返回发送方法(数据,*args)
ConnectionAbortedError: [WinError 10053] 已建立的连接被主机中的软件中止

在处理上述异常的过程中,又出现了一个异常:

回溯(最近一次通话最后):
文件“C:\ProgramData\Anaconda3\lib\site-packages\eventlet\hubs\hub.py”,第 461 行,在 fire_timers
计时器()
__call__ 中的文件“C:\ProgramData\Anaconda3\lib\site-packages\eventlet\hubs\timer.py”,第 59 行
CB(参数,*千瓦)
_do_acquire 中的文件“C:\ProgramData\Anaconda3\lib\site-packages\eventlet\semaphore.py”,第 147 行
服务员.switch()
文件“C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenthread.py”,第 221 行,在 main
结果 = 函数(参数,* kwargs)
文件“C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py”,第 818 行,在 process_request
proto.__init__(conn_state, self)
文件“C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py”,第 359 行,在 __init__
self.finish()
文件“C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py”,第 732 行,完成
BaseHTTPServer.BaseHTTPRequestHandler.finish(self)
文件“C:\ProgramData\Anaconda3\lib\socketserver.py”,第 784 行,完成
self.wfile.close()
文件“C:\ProgramData\Anaconda3\lib\socket.py”,第 607 行,写入
返回 self._sock.send(b)
文件“C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenio\base.py”,第 397 行,在发送中
return self._send_loop(self.fd.send, data, flags)
_send_loop 中的文件“C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenio\base.py”,第 384 行
返回发送方法(数据,*args)
ConnectionAbortedError: [WinError 10053] 已建立的连接被主机中的软件中止```

可以确认当哨兵完全禁用时此错误消失。 如果 gunicorn 足够强大来处理这个问题,那就太好了。

撞@benoitc

可以确认当哨兵完全禁用时此错误消失。 如果 gunicorn 足够强大来处理这个问题,那就太好了。

我发现禁用哨兵的FlaskIntegration也会使错误消失。

看到类似的行为。 在生产中使用 New Relic 会导致 flask-socketio 出现此错误。 在开发中,需要在flask-socketio初始化之前加载werkzeug调试器中间件(因此不适用于engineio的wsgi应用)。 问题是生产是我真的不希望错误跳闸的地方。

无法替换 gunicorn 配置的 post_request 中的响应,但我尝试将状态代码强制到 resp.status_code 上。 不过也没用。

通过将 Sentry 的 FlaskIntegration 与 Gunicorn 和 Flask-SocketIO 一起使用,可以重现此错误。 有可能很快解决吗?

@Canicio我们想尝试消除错误,即使在禁用集成后,错误仍然存​​在。

有没有人有可共享的代码/ @benoitc的最小示例?

当然:

import sentry_sdk
from flask import Flask
from flask_socketio import SocketIO
from sentry_sdk.integrations.flask import FlaskIntegration

sentry_sdk.init(
    dsn="https://[email protected]/0",
    integrations=[FlaskIntegration()]
)

app = Flask(__name__)
socketio = SocketIO(app)

@app.route('/')
def index():
    return '''
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js"></script>
<script>
    var socket = io()
</script>

要求:

flask
sentry-sdk[flask]
flask-socketio
eventlet

例如 gunicorn 配置:

bind = '[::]:4444'
worker_class = 'eventlet'
accesslog = '-'

在加载/时,它将连接到 websocket。 在 websocket 断开连接(例如导航离开、刷新)时,将产生如下异常:

[2020-09-23 07:24:49 +0000] [16303] [ERROR] Error handling request /socket.io/?EIO=3&transport=websocket&sid=29f4c1adfac343d6bc6db56acf8fd0ee
Traceback (most recent call last):
  File "/home/ziddey/projects/sentry/venv_sentry/lib/python3.8/site-packages/gunicorn/workers/base_async.py", line 55, in handle
    self.handle_request(listener_name, req, client, addr)
  File "/home/ziddey/projects/sentry/venv_sentry/lib/python3.8/site-packages/gunicorn/workers/base_async.py", line 115, in handle_request
    resp.close()
  File "/home/ziddey/projects/sentry/venv_sentry/lib/python3.8/site-packages/gunicorn/http/wsgi.py", line 402, in close
    self.send_headers()
  File "/home/ziddey/projects/sentry/venv_sentry/lib/python3.8/site-packages/gunicorn/http/wsgi.py", line 318, in send_headers
    tosend = self.default_headers()
  File "/home/ziddey/projects/sentry/venv_sentry/lib/python3.8/site-packages/gunicorn/http/wsgi.py", line 299, in default_headers
    elif self.should_close():
  File "/home/ziddey/projects/sentry/venv_sentry/lib/python3.8/site-packages/gunicorn/http/wsgi.py", line 219, in should_close
    if self.status_code < 200 or self.status_code in (204, 304):
AttributeError: 'Response' object has no attribute 'status_code'
2001:470:1f07:7eb:9dd4:254c:35d7:236c - - [23/Sep/2020:07:24:49 +0000] "GET /socket.io/?EIO=3&transport=websocket&sid=29f4c1adfac343d6bc6db56acf8fd0ee HTTP/1.1" 500 0 "-" "-"

注意:我自己从未真正使用过哨兵。 这只是来自哨兵入门页面。 示例dsn适用于我们的测试。

然后评论integrations=[FlaskIntegration()]将消除错误(当然有效地禁用哨兵)。

对于它的价值,可以使用 gevent-websocket 代替 eventlet 而不会出错。 但是,它似乎可以处理所有请求..

好吧,玩了一些。 看起来 sentry/newrelic 包装了响应。 如果没有哨兵,我们会按预期得到<eventlet.wsgi._AlreadyHandled object at 0x7fd0f5b1c0d0> ,并且 gunicorn 的 EventletWorker.is_already_handled() 将停止迭代。

但是,当使用哨兵时,这会变成类似<sentry_sdk.integrations.wsgi._ScopedResponse object at 0x7f30155a5100>的东西,检查失败

相反,我们可以偷看呼吸器,看看它是否是空的。 明天再看。

好吧,这是我想出的解决方法:

eventlet_fix.py:
请参阅下面的编辑

在我的 gunicorn config.py: worker_class = 'eventlet_fix.EventletWorker中。

问题是 sentry/newrelic 包装了响应,所以我们不能简单地对照 eventlet 的ALREADY_HANDLED来检查它。 由于已处理请求的性质是 gunicorn 的start_response没有被调用,因此我们可以检查是否存在响应状态。

所以我劫持了 wsgi 调用,然后检查响应状态,并根据需要破解响应值。 这允许请求仍然由 gunicorn 记录。 相反,如果希望保持原始行为,则可以改为提高StopIteration

将状态设置为 101 适合我们这里的用例(flask-socketio websocket),但除此之外,将其保留为 None 也可以,因为headers_sentshould_close被强制为 True。

同样,这假设如果status未设置,则start_response未调用,因此该请求必须已在外部“已处理”。

编辑:不好。 将需要重新评估。 如果请求需要时间来执行,则在检查resp.status之前不会调用 $#$ start_response $#$。

edit2:这是一个带有被黑响应迭代器的固定版本:

from functools import wraps

from gunicorn.workers.geventlet import EventletWorker as _EventletWorker


class HackedResponse:
    def __init__(self, respiter, resp):
        self.respiter = iter(respiter)
        self.resp = resp
        if hasattr(respiter, "close"):
            self.close = respiter.close

    def __iter__(self):
        return self

    def __next__(self):
        try:
            return next(self.respiter)
        except StopIteration:
            if not self.resp.status:
                self.resp.status = "101"  # logger derives status code from status instead of using status_code
                self.resp.status_code = 101  # not actually needed since headers_sent/force_close result in status_code not being checked anymore
                self.resp.headers_sent = True
                self.resp.force_close()
            raise


def wsgi_decorator(wsgi):
    @wraps(wsgi)
    def wrapper(environ, start_response):
        respiter = wsgi(environ, start_response)
        resp = start_response.__self__
        return HackedResponse(respiter, resp)

    return wrapper


class EventletWorker(_EventletWorker):
    def load_wsgi(self):
        super().load_wsgi()
        self.wsgi = wsgi_decorator(self.wsgi)

显然这只是一个猴子补丁。 实际的修复可能会出现在 base_async.py 中的handle_request中。 关键可能是(间接)检查start_response是否在迭代respiter $ 后被调用,或者通过检查resp.status (仅调用start_response )或resp.headers_sent (确认我们确实响应了请求)。

@benoitc
@ziddey找到了解决问题的方法。

@ziddey针对您的示例的快速问题(因为我没有使用哨兵)。

  • 错误仅影响哨兵还是请求也停止了,即工作人员正在终止(我怀疑如果响应被包装,它会这样做)?
  • 即使响应被包装,您是否希望在那里包装一些东西或清理请求?

@benoitc目前无法测试,但查看上面的追溯https://github.com/benoitc/gunicorn/issues/1852#issuecomment -697189261 和https://github.com/benoitc/gunicorn/blob/4ae2a05c37b332773997f90ba7542713b9bf8274/ gunicorn/workers/base_async.py#L107 -L140

通常情况下, is_already_handled会返回 True,它会在这里结束。

但是,由于响应已包装,因此该方法不起作用。 相反,执行继续进行,在第 115 行失败: resp.close()尝试发送标头,但从未调用start_response ,因此没有状态代码。 即使这样做了,它最终仍然会明显失败。

这会导致 AttributeError 被重新引发并假定由handle_error处理。 由于该请求已在外部处理,因此除了记录垃圾邮件之外没有其他危害。

关于 Sentry,我不能说太多——我也没有使用它。

但是有一个细节:当前已经处理的机制导致没有访问日志记录。 我认为这在技术上是有道理的,因为无法知道它是如何在外部处理的。 在我被黑的响应中,我将状态代码强制为 101,将headers_sent设置为 True,以便处理程序可以继续,并且请求仍然会记录访问权限。

检查resp.status是确定是否调用start_response的明确测试。

@benoitc重温这一点。 为了更明确地得出该请求已被处理的结论, environ['gunicorn.socket']可以代替底层对象的某种代理。 这样,可以在直接访问套接字时记录它(例如,将get_socket()包装为 eventlet),并用于is_already_handled之类的东西

如果需要访问日志,它仍然需要破解响应状态。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

thomasjungblut picture thomasjungblut  ·  3评论

Abraxos picture Abraxos  ·  4评论

benoitc picture benoitc  ·  4评论

Bogdanp picture Bogdanp  ·  3评论

haolujun picture haolujun  ·  3评论