Gunicorn: Ancien bogue reproduit : l'objet 'Response' n'a pas d'attribut 'status_code' dans wsgi.py avec websockets

Créé le 9 août 2018  ·  33Commentaires  ·  Source: benoitc/gunicorn

Tout comme ce vieux problème 1210 l'a dit, gunicorn enregistre une erreur lorsque le client se déconnecte, et mon environnement est :

  • Debian GNU/Linux 7.8

  • nginx

  • Python3.4

  • gunicorn(19.8.1) (avec un ou plusieurs travailleurs)

  • Flask-SocketIO, le client spécifie le transport websocket

Tout fonctionne bien, y compris les clients, à l'exception de ce journal d'erreurs, deux instances de production indépendantes du cloud, toutes deux enregistrées de manière persistante, mais je ne peux pas le reproduire sur ma machine de développement, qui est un mac.

Merci beaucoup pour votre aide.

Erreur de traitement de la demande /socket.io/?EIO=3&transport=websocket
Traceback (dernier appel le plus récent) :
Fichier "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/workers/async.py", ligne 56, dans handle
self.handle_request(listener_name, req, client, addr)
Fichier "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/workers/async.py", ligne 116, dans handle_request
resp.close()
Fichier "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py", ligne 409, en fin
self.send_headers()
Fichier "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py", ligne 325, dans send_headers
envoyer = self.default_headers()
Fichier "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py", ligne 306, dans default_headers
elif self.should_close() :
Fichier "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py", ligne 229, dans should_close
si self.status_code < 200 ou self.status_code dans (204, 304) :
AttributeError : l'objet 'Response' n'a pas d'attribut 'status_code'

Feedback Requested unconfirmed ThirdPartFlask

Commentaire le plus utile

Bump @benoitc

Tous les 33 commentaires

avez-vous un exemple simple pour le reproduire? Veuillez également essayer avec le dernier master si possible.

Auparavant, j'ai essayé plusieurs fois dans mon environnement de développement local, qui est le même code d'application avec l'environnement de production, mais je ne peux pas le reproduire.

Et j'ai vérifié le journal des versions de la version 19.9.0 , pas trouvé quelque chose de lié , je garderai
en regardant ce journal d'erreurs, si je trouve quelque chose de nouveau, je posterais ici.

J'ai aussi ce problème, en particulier lorsque je force toute ma connexion du client au protocole websocket. Mes paramètres sont les mêmes que BoWuGit. Si allow polling protocol before upgrade, cela ne s'affiche pas, mais une autre erreur :
`
[ERREUR] Erreur de traitement de la demande /socket.io/?EIO=3&transport=polling&t=MPRHUoV&sid=cd64be7c940e474d8728b114c3fb9bbe

Traceback (dernier appel le plus récent) :
Fichier "/usr/local/lib/python3.6/site-packages/gunicorn/workers/async.py", ligne 56, dans handle
self.handle_request(listener_name, req, client, addr)

Fichier "/usr/local/lib/python3.6/site-packages/gunicorn/workers/async.py", ligne 107, dans handle_request
respiter = self.wsgi(environ, resp.start_response)

Fichier "/usr/local/lib/python3.6/site-packages/flask/app.py", ligne 1994, dans __call__
retourner self.wsgi_app(environ, start_response)

Fichier "/usr/local/lib/python3.6/site-packages/flask_socketio/__init__.py", ligne 43, dans __call__
start_response)

Fichier "/usr/local/lib/python3.6/site-packages/engineio/middleware.
py", ligne 47, dans __call__
retourner self.engineio_app.handle_request(environ, start_response)

Fichier "/usr/local/lib/python3.6/site-packages/socketio/server.py", ligne 360, dans handle_request
retourner self.eio.handle_request(environ, start_response)

Fichier "/usr/local/lib/python3.6/site-packages/engineio/server.py", ligne 279, dans handle_request
socket = self._get_socket(sid)

Fichier "/usr/local/lib/python3.6/site-packages/engineio/server.py", ligne 439, dans _get_socket
raise KeyError('La session est déconnectée')
`
Mais je soupçonne que cela pourrait avoir quelque chose à voir les uns avec les autres, puisque je force la connexion à être websocket, cette erreur n'a plus été vue.

Avoir ce problème également avec gunicorn 19.9.0 et Flask-socketIO 3.0.2, lors de l'utilisation de l'eventlet 0.24.1

AttributeError : l'objet 'Response' n'a pas d'attribut 'status_code'

Vous rencontrez également ce problème avec les exigences suivantes :

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

Message d'erreur lors de la fermeture d'un navigateur Web avec une connexion socket ouverte :

 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'

Il semble que ce problème ait été résolu dans la dernière version de python-engineio ..

J'ai testé avec la dernière version de python-engineio (2.3.2), cela ne fonctionne toujours pas.

Des news sur ce sujet ? J'obtiens la même erreur lors de l'utilisation de sentry-python

J'ai le même problème

eventlet : 0.25.1
fiole-socketio : 4.2.1
gunicorne : 19.9.0

image

image

comment le reproduire ? cna vous fournissez un exemple simple?

Je ne sais pas non plus comment le reproduire, mais cela arrive SOUVENT lorsque j'actualise une page sur mon application gunicorn

Rencontrez le même problème, et mon environnement est le même que @eazow alors que gunicorn == 20.0.4.
Il semble que le problème se soit produit après l'installation de Sentry pour le suivi des bogues.
Les problèmes peuvent être reproduits par

  1. actualiser la page (ne pas ouvrir une nouvelle page)
  2. fermer la page

Fait intéressant, l'ouverture d'une nouvelle page ne produira pas le problème. Pas certain de pourquoi. Merci!

J'ai le même problème que @cowbonlin . Même version gunicorn aussi.

Après avoir installé sentinelle, nous obtenons des quantités folles de cette erreur. Bien que j'aie du mal à dire si cela s'est toujours produit ou non - puisque nous n'avons pas suivi les erreurs avant la sentinelle.

Bien que cela ne semble pas influencer les fonctionnalités réelles de notre serveur, il ne s'agit que d'une tonne de spam.

Nous vivons la même chose. Sentry installé mais désactivé. Des idées?

Même problème avec la sentinelle installée.

Avez-vous un exemple qui le reproduise sans sentinelle (désactivé ou non) ?

De plus, au lieu d'un espace de noms, j'appuie manuellement sur /api.

De plus, au lieu d'un espace de noms, j'appuie manuellement sur /api.

Qu'est-ce que ça veut dire ? Cette sentinelle est-elle liée ?

De plus, au lieu d'un espace de noms, j'appuie manuellement sur /api.

Qu'est-ce que ça veut dire ? Cette sentinelle est-elle liée ?

Non, cela est lié aux espaces de noms socket.io. J'ai essayé de les supprimer, et même après les avoir supprimés, j'obtiens l'erreur. J'obtiens cette autre erreur sur une machine locale sans gunicorn ou nginx cependant, qui peut être liée.

Voici mes exigences :

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

Voici mon code flask-socketio côté serveur :

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

Et voici mon code React socket io côté client :

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

Faites-moi savoir si cela aide. Sur Ubuntu, l'erreur ressemble à celle ci-dessus, et localement sur Windows, elle ressemble à ceci :
```Traceback (appel le plus récent en dernier) :
Fichier "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", ligne 599, dans handle_one_response
écrire (b'')
Fichier "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", ligne 491, en écriture
raise AssertionError("write() avant start_response()")
AssertionError : write() avant start_response()

Lors du traitement de l'exception ci-dessus, une autre exception s'est produite :

Traceback (dernier appel le plus récent) :
Fichier "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", ligne 357, dans __init__
self.handle()
Fichier "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", ligne 390, dans handle
self.handle_one_request()
Fichier "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", ligne 466, dans handle_one_request
self.handle_one_response()
Fichier "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", ligne 609, dans handle_one_response
écrire (err_body)
Fichier "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", ligne 538, en écriture
wfile.flush()
Fichier "C:\ProgramData\Anaconda3\lib\socket.py", ligne 607, en écriture
retour self._sock.send(b)
Fichier "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenio\base.py", ligne 397, en envoi
return self._send_loop(self.fd.send, data, flags)
Fichier "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenio\base.py", ligne 384, dans _send_loop
retourner send_method(données, *args)
ConnectionAbortedError : [WinError 10053] Une connexion établie a été abandonnée par le logiciel de votre ordinateur hôte

Lors du traitement de l'exception ci-dessus, une autre exception s'est produite :

Traceback (dernier appel le plus récent) :
Fichier "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\hubs\hub.py", ligne 461, dans fire_timers
minuteur()
Fichier "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\hubs\timer.py", ligne 59, dans __call__
cb( arguments, * kw)
Fichier "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\semaphore.py", ligne 147, dans _do_acquire
serveur.switch()
Fichier "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenthread.py", ligne 221, dans main
résultat = fonction( args, * kwargs)
Fichier "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", ligne 818, dans process_request
proto.__init__(conn_state, self)
Fichier "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", ligne 359, dans __init__
self.finish()
Fichier "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", ligne 732, en finition
BaseHTTPServer.BaseHTTPRequestHandler.finish(self)
Fichier "C:\ProgramData\Anaconda3\lib\socketserver.py", ligne 784, en finition
self.wfile.close()
Fichier "C:\ProgramData\Anaconda3\lib\socket.py", ligne 607, en écriture
retour self._sock.send(b)
Fichier "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenio\base.py", ligne 397, en envoi
return self._send_loop(self.fd.send, data, flags)
Fichier "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenio\base.py", ligne 384, dans _send_loop
retourner send_method(données, *args)
ConnectionAbortedError : [WinError 10053] Une connexion établie a été interrompue par le logiciel de votre machine hôte```

Peut confirmer que cette erreur disparaît lorsque la sentinelle est complètement désactivée. Ce serait formidable si gunicorn était assez robuste pour faire face à cela.

Bump @benoitc

Peut confirmer que cette erreur disparaît lorsque la sentinelle est complètement désactivée. Ce serait formidable si gunicorn était assez robuste pour faire face à cela.

J'ai constaté que la désactivation du FlaskIntegration de Sentry faisait également disparaître l'erreur.

Voir un comportement similaire. L'utilisation de New Relic en production provoque cette erreur avec flask-socketio. En développement, le middleware de débogage werkzeug doit être chargé avant l'initialisation de flask-socketio (il n'est donc pas appliqué à l'application wsgi d'engineio). Le problème est que la production est là où je ne veux vraiment pas que les erreurs se déclenchent.

Impossible de remplacer la réponse dans post_request de gunicorn config, mais j'ai essayé de forcer un code d'état sur resp.status_code. Cela n'a pas pris cependant.

Cette erreur est reproductible en utilisant FlaskIntegration de Sentry avec Gunicorn et Flask-SocketIO. Est-il possible de le résoudre rapidement ?

@Canicio nous avons pensé essayer cela pour nous débarrasser de l'erreur et même après avoir désactivé l'intégration, l'erreur persiste.

Quelqu'un a-t-il un code partageable / un exemple minimal pour que @benoitc puisse s'en sortir ?

Sûr:

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>

exigences:

flask
sentry-sdk[flask]
flask-socketio
eventlet

exemple de configuration gunicorn :

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

Lors du chargement / , il se connectera au websocket. Lors de la déconnexion du websocket (par exemple, naviguer, rafraîchir), produira une exception comme celle-ci :

[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 "-" "-"

Remarque : Je n'ai jamais utilisé Sentry moi-même. Ceci vient juste de la page de démarrage de la sentinelle. L'exemple dsn fonctionne bien pour notre test.

Commenter integrations=[FlaskIntegration()] éliminera alors l'erreur (bien sûr, en désactivant efficacement la sentinelle).

Pour ce que ça vaut, gevent-websocket peut être utilisé à la place de eventlet sans erreur. Cependant, il semble alors traiter toutes les demandes.

Ok, j'ai joué un peu. On dirait que sentry/newrelic enveloppe la réponse. Sans sentinelle, nous obtenons <eventlet.wsgi._AlreadyHandled object at 0x7fd0f5b1c0d0> comme prévu et EventletWorker.is_already_handled() de gunicorn arrêtera l'itération.

Cependant, lors de l'utilisation de la sentinelle, cela devient quelque chose comme <sentry_sdk.integrations.wsgi._ScopedResponse object at 0x7f30155a5100> la place, échouant la vérification

Au lieu de cela, nous pourrions jeter un coup d'œil au respiter pour voir s'il est vide. Je chercherai plus loin demain.

Très bien, voici la solution de contournement que j'ai trouvée :

eventlet_fix.py :
voir modification ci-dessous

Et dans mon gunicorn config.py : worker_class = 'eventlet_fix.EventletWorker .

Le problème est que sentry/newrelic encapsule les réponses, nous ne pouvons donc pas simplement le vérifier par rapport à ALREADY_HANDLED d'eventlet. Étant donné que la nature d'une requête déjà traitée est que le start_response de gunicorn n'est pas appelé, nous pouvons plutôt vérifier la présence d'un statut de réponse.

J'ai donc détourné l'appel wsgi pour ensuite vérifier l'état de la réponse et pirater les valeurs de réponse si nécessaire. Cela permet à la demande d'être toujours enregistrée par gunicorn. Si, à la place, on souhaite conserver le comportement d'origine, StopIteration peut être levé à la place.

Le piratage du statut à 101 est approprié pour notre cas d'utilisation ici (flask-socketio websocket), mais sinon, le laisser sur None fonctionne également puisque headers_sent et should_close sont forcés à True.

Encore une fois, cela suppose que si status n'est pas défini, start_response n'a pas été appelé, et donc la requête doit avoir été "déjà traitée" en externe.

édit : pas bon. Faudra réévaluer. Si la requête prend du temps à s'exécuter, start_response ne sera pas appelé avant que resp.status ne soit vérifié.

edit2 : Voici une version corrigée avec un itérateur de réponse piraté :

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)

De toute évidence, ce n'est qu'un patch de singe. Le correctif réel pourrait potentiellement aller dans handle_request dans base_async.py. La clé peut être de vérifier (indirectement) si start_response a été appelé après avoir parcouru respiter , soit en vérifiant resp.status (juste start_response appelé) ou resp.headers_sent (confirmation que nous avons effectivement répondu à la demande).

@benoitc
@ziddey a trouvé un moyen de résoudre le problème.

@ziddey questions rapides pour votre exemple (car je n'utilise pas sentinelle).

  • L'erreur n'affecte-t-elle que la sentinelle ou la demande est-elle également arrêtée, c'est-à-dire que le travailleur se termine (ce que je soupçonne qu'il le fait si la réponse est enveloppée)?
  • vous attendez-vous à ce que quelque chose soit enveloppé là-bas ou que le nettoyage de la demande même si la réponse est enveloppée serait OK?

@benoitc pas en mesure de tester actuellement, mais en regardant la trace ci-dessus https://github.com/benoitc/gunicorn/issues/1852#issuecomment -697189261 et https://github.com/benoitc/gunicorn/blob/4ae2a05c37b332773997f90ba7542713b9bf8274/ gunicorn/travailleurs/base_async.py#L107 -L140

Normalement, is_already_handled renverrait True et cela se terminerait ici.

Cependant, comme la réponse est encapsulée, cette méthode ne fonctionne pas. Au lieu de cela, l'exécution progresse, échouant à la ligne 115 : resp.close() tente d'envoyer des en-têtes, mais start_response n'a jamais été appelé, il n'y a donc pas de code d'état. Même si c'était le cas, cela finirait par échouer évidemment.

Cela se traduit par un AttributeError qui est relancé et supposément géré par handle_error . Étant donné que la demande a déjà été traitée en externe, il n'y a aucun mal ici autre que le spam de journal.

Je ne peux pas en dire trop sur Sentry - je ne l'utilise pas non plus.

Un détail cependant : le mécanisme actuel déjà géré n'entraîne aucune journalisation des accès. Je suppose que cela a du sens techniquement puisqu'il n'y a aucun moyen de savoir comment cela a été géré en externe. Dans ma réponse piratée, je force le code d'état à 101, avec headers_sent comme True afin que le gestionnaire puisse continuer et que la demande obtienne toujours un accès enregistré.

Vérifier resp.status est un test définitif pour déterminer si start_response a été appelé.

@benoitc revisitant cela. Pour conclure plus définitivement que la requête a déjà été traitée, environ['gunicorn.socket'] pourrait plutôt être une sorte de proxy pour l'objet sous-jacent. De cette façon, il peut être enregistré lors de l'accès direct au socket (par exemple, envelopper get_socket() pour eventlet), et utilisé pour quelque chose comme is_already_handled

Cela nécessiterait toujours de pirater un état de réponse si la journalisation des accès est souhaitée.

Cette page vous a été utile?
0 / 5 - 0 notes