Gunicorn: 再現された古いバグ:「Response」オブジェクトには、WebSocketを使用したwsgi.pyに「status_code」属性がありません

作成日 2018年08月09日  ·  33コメント  ·  ソース: benoitc/gunicorn

この古い問題1210が言ったように、クライアントが切断するとgunicornはエラーをログに記録し、私の環境は次のようになります。

  • Debian GNU / Linux 7.8

  • nginx

  • Python3.4

  • gunicorn(19.8.1)(1人または複数のワーカーを含む)

  • Flask-SocketIO、クライアントはWebSocketトランスポートを指定します

このエラーログを除いて、クライアントを含めてすべてがうまく機能します。2つのクラウドに依存しない本番インスタンスは両方とも永続的にログに記録されますが、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)
handle_requestのファイル「/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/workers/async.py」、116行目
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()
default_headersのファイル "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py"、行306
elif self.should_close():
ファイル "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py"、229行目、should_close
self.status_code <200または(204、304)のself.status_codeの場合:
AttributeError: 'Response'オブジェクトに属性 'status_code'がありません

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)

handle_requestのファイル「/usr/local/lib/python3.6/site-packages/gunicorn/workers/async.py」、107行目
respiter = self.wsgi(environ、resp.start_response)

ファイル "/usr/local/lib/python3.6/site-packages/flask/app.py"、1994行、__ call__
self.wsgi_app(environ、start_response)を返します

ファイル "/usr/local/lib/python3.6/site-packages/flask_socketio/__init__.py"、43行目、__ call__
start_response)

ファイル "/usr/local/lib/python3.6/site-packages/engineio/middleware。
py」、47行目、__ call__
self.engineio_app.handle_request(environ、start_response)を返します

handle_requestのファイル "/usr/local/lib/python3.6/site-packages/socketio/server.py"、行360
self.eio.handle_request(environ、start_response)を返します

handle_requestのファイル「/usr/local/lib/python3.6/site-packages/engineio/server.py」、279行目
socket = self._get_socket(sid)

ファイル "/usr/local/lib/python3.6/site-packages/engineio/server.py"、439行目、_get_socket
KeyErrorを発生させます( 'セッションが切断されました')
`
しかし、接続をWebSocketに強制するため、このエラーは二度と見られないため、相互に関係があるのではないかと思います。

イベントレット0.24.1を使用している場合、gunicorn19.9.0およびFlask-socketIO3.0.2でもこの問題が発生します

AttributeError: 'Response'オブジェクトに属性 'status_code'がありません

また、次の要件でこの問題が発生しています。

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)でテストしましたが、まだ機能しません。

この問題に関するニュースはありますか? 歩哨Pythonを使用すると同じエラーが発生します

私は同じ問題を抱えています

イベントレット:0.25.1
フラスコ-ソケット:4.2.1
gunicorn:19.9.0

image

image

それを再現する方法は? cnaあなたは簡単な例を提供しますか?

再現方法もわかりませんが、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

これは、サーバー側の私のフラスコソケットコードです。

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ではローカルで次のようになります。
`` `トレースバック(最後の最後の呼び出し):
handle_one_responseのファイル "C:\ ProgramData \ Anaconda3 \ lib \ site-packages \ eventlet \ wsgi.py"、行599
write(b '')
ファイル「C:\ ProgramData \ Anaconda3 \ lib \ site-packages \ eventlet \ wsgi.py」、491行目、書き込み
AssertionError( "write()before start_response()")を発生させます
AssertionError:start_response()の前にwrite()

上記の例外の処理中に、別の例外が発生しました。

トレースバック(最後の最後の呼び出し):
__init__のファイル "C:\ ProgramData \ Anaconda3 \ lib \ site-packages \ eventlet \ wsgi.py"、行357
self.handle()
ファイル "C:\ ProgramData \ Anaconda3 \ lib \ site-packages \ eventlet \ wsgi.py"、行390、ハンドル
self.handle_one_request()
handle_one_requestのファイル "C:\ ProgramData \ Anaconda3 \ lib \ site-packages \ eventlet \ wsgi.py"、行466
self.handle_one_response()
handle_one_responseのファイル "C:\ ProgramData \ Anaconda3 \ lib \ site-packages \ eventlet \ wsgi.py"、行609
write(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、送信
self._send_loop(self.fd.send、data、flags)を返します
_send_loopのファイル "C:\ ProgramData \ Anaconda3 \ lib \ site-packages \ eventlet \ greenio \ base.py"、行384
send_method(data、* args)を返します
ConnectionAbortedError:[WinError10053]確立された接続がホストマシンのソフトウェアによって中止されました

上記の例外の処理中に、別の例外が発生しました。

トレースバック(最後の最後の呼び出し):
ファイル「C:\ ProgramData \ Anaconda3 \ lib \ site-packages \ eventlet \ hubs \ hub.py」の461行目。fire_timers
タイマー()
ファイル "C:\ ProgramData \ Anaconda3 \ lib \ site-packages \ eventlet \ Hubs \ timer.py"、59行目、__ call__
cb( args、* kw)
_do_acquireのファイル "C:\ ProgramData \ Anaconda3 \ lib \ site-packages \ eventlet \ semaphore.py"、行147
waiter.switch()
ファイル "C:\ ProgramData \ Anaconda3 \ lib \ site-packages \ eventlet \ greenthread.py"、行221、メイン
結果= function( args、* kwargs)
process_requestのファイル "C:\ ProgramData \ Anaconda3 \ lib \ site-packages \ eventlet \ wsgi.py"、行818
proto .__ init __(conn_state、self)
__init__のファイル "C:\ ProgramData \ Anaconda3 \ lib \ site-packages \ eventlet \ wsgi.py"、行359
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、送信
self._send_loop(self.fd.send、data、flags)を返します
_send_loopのファイル "C:\ ProgramData \ Anaconda3 \ lib \ site-packages \ eventlet \ greenio \ base.py"、行384
send_method(data、* args)を返します
ConnectionAbortedError:[WinError10053]確立された接続がホストマシンのソフトウェアによって中止されました `` `

歩哨が完全に無効になると、このエラーが消えることを確認できます。 gunicornがこれに対処するのに十分堅牢であるなら素晴らしいでしょう。

バンプ@benoitc

歩哨が完全に無効になると、このエラーが消えることを確認できます。 gunicornがこれに対処するのに十分堅牢であるなら素晴らしいでしょう。

セントリーのFlaskIntegrationを無効にすると、エラーも消えることがわかりました。

同様の動作を確認します。 プロダクションでNewRelicを使用すると、flask-socketioでこのエラーが発生します。 開発では、flask-socketioを初期化する前にwerkzeugデバッガミドルウェアをロードする必要があります(したがって、engineioのwsgiアプリには適用されません)。 問題は、本番環境では、エラーが発生したくない場合です。

gunicorn configの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をエラーなしで使用できることです。 ただし、すべてのリクエストを処理しているようです。

さて、遊んでみました。 歩哨/ newrelicが応答をラップしているように見えます。 歩哨がないと、期待どおりに<eventlet.wsgi._AlreadyHandled object at 0x7fd0f5b1c0d0>が得られ、gunicornのEventletWorker.is_already_handled()は反復を停止します。

ただし、歩哨を使用する場合、これは代わりに<sentry_sdk.integrations.wsgi._ScopedResponse object at 0x7f30155a5100>のようになり、チェックに失敗します

代わりに、respiterを覗いて、空かどうかを確認できます。 明日はさらに見えます。

了解しました。これが私が思いついた回避策です。

eventlet_fix.py:
以下の編集を参照してください

そして私のgunicornconfig.pyで: worker_class = 'eventlet_fix.EventletWorker

問題は、sentry / newrelicが応答をラップするため、eventletのALREADY_HANDLEDに対して単純にチェックできないことです。 すでに処理されたリクエストの性質上、gunicornのstart_responseは呼び出されないため、代わりに応答ステータスの存在を確認できます。

そこで、wsgi呼び出しを乗っ取って応答ステータスを確認し、必要に応じて応答値をハックしました。 これにより、リクエストは引き続きgunicornによってログに記録されます。 代わりに、元の動作を維持したい場合は、代わりにStopIterationを上げることができます。

ステータスを101にハックすることは、ここでのユースケース(flask-socketio websocket)に適していますが、それ以外の場合は、 headers_sentshould_closeが強制的にTrueになるため、Noneのままにしておくこともできます。

繰り返しになりますが、これは、 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に含まれる可能性があります。 重要なのは、 resp.statusstart_responseが呼び出されただけ)またはresp.headers_sentのいずれかをチェックすることにより、 respiterを反復処理した後に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によって処理されると想定されます。 リクエストはすでに外部で処理されているため、ログスパム以外に害はありません。

セントリーについてはあまり言えません。私も使用していません。

ただし、詳細が1つあります。現在処理済みのメカニズムでは、アクセスログが記録されません。 外部でどのように処理されたかを知る方法がないため、これは技術的には理にかなっていると思います。 ハッキングされた応答では、ステータスコードを101に強制し、 headers_sentをTrueにして、ハンドラーが続行でき、リクエストが引き続きアクセスログに記録されるようにします。

resp.statusをチェックすることは、 start_responseが呼び出されたかどうかを判断するための決定的なテストです。

@benoitcがこれを再考します。 リクエストがすでに処理されているとより明確に結論付けるために、 environ['gunicorn.socket']は、代わりに、基になるオブジェクトのある種のプロキシである可能性があります。 そうすれば、ソケットが直接アクセスされたときに記録され(たとえば、イベントレットの場合get_socket()をラップする)、 is_already_handledのようなものに使用できます。

アクセスログが必要な場合でも、応答ステータスをハッキングする必要があります。

このページは役に立ちましたか?
0 / 5 - 0 評価