Gunicorn: Error antiguo reproducido: el objeto 'Respuesta' no tiene el atributo 'código_estado' en wsgi.py con websockets

Creado en 9 ago. 2018  ·  33Comentarios  ·  Fuente: benoitc/gunicorn

Tal como decía este problema anterior 1210 , gunicorn registra un error cuando el cliente se desconecta, y mi entorno es:

  • Debian GNU/Linux 7.8

  • nginx

  • Python3.4

  • gunicorn(19.8.1) (con uno o varios trabajadores)

  • Flask-SocketIO, el cliente especifica el transporte websocket

Todo funciona bien, incluidos los clientes, a excepción de este registro de errores, dos instancias de producción independientes de la nube registran de forma persistente, pero no puedo reproducirlo en mi máquina de desarrollo, que es una Mac.

Muchas gracias por tu ayuda.

Solicitud de manejo de errores /socket.io/?EIO=3&transport=websocket
Rastreo (llamadas recientes más última):
Archivo "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/workers/async.py", línea 56, en handle
self.handle_request(listener_name, req, client, addr)
Archivo "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/workers/async.py", línea 116, en handle_request
resp.cerrar()
Archivo "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py", línea 409, en cierre
self.send_headers()
Archivo "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py", línea 325, en send_headers
enviar = self.default_headers()
Archivo "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py", línea 306, en default_headers
elif self.should_close():
Archivo "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py", línea 229, en should_close
si self.status_code < 200 o self.status_code en (204, 304):
AttributeError: el objeto 'Respuesta' no tiene atributo 'status_code'

Feedback Requested unconfirmed ThirdPartFlask

Comentario más útil

Bump @benoitc

Todos 33 comentarios

¿Tienes algún ejemplo sencillo para reproducirlo? También intente con el último maestro si es posible.

Anteriormente, lo intenté varias veces en mi entorno de desarrollo local, que es el mismo código de aplicación con el entorno de producción, pero no puedo reproducirlo.

Y revisé el registro de lanzamiento de la versión 19.9.0, no encontré nada relacionado, lo mantendré
viendo este registro de errores, si encuentro algo nuevo, lo publicaría aquí.

Yo también tengo este problema, específicamente cuando fuerzo toda mi conexión del cliente al protocolo websocket. Mi configuración es la misma que BoWuGit. Si permite el protocolo de sondeo antes de la actualización, esto no aparece, sino otro error:
`
[ERROR] Error al manejar la solicitud /socket.io/?EIO=3&transport=polling&t=MPRHUoV&sid=cd64be7c940e474d8728b114c3fb9bbe

Rastreo (llamadas recientes más última):
Archivo "/usr/local/lib/python3.6/site-packages/gunicorn/workers/async.py", línea 56, en handle
self.handle_request(listener_name, req, client, addr)

Archivo "/usr/local/lib/python3.6/site-packages/gunicorn/workers/async.py", línea 107, en handle_request
respiter = self.wsgi(entorno, resp.start_response)

Archivo "/usr/local/lib/python3.6/site-packages/flask/app.py", línea 1994, en __call__
devolver self.wsgi_app(entorno, start_response)

Archivo "/usr/local/lib/python3.6/site-packages/flask_socketio/__init__.py", línea 43, en __call__
inicio_respuesta)

Archivo "/usr/local/lib/python3.6/site-packages/engineio/middleware.
py", línea 47, en __call__
volver self.engineio_app.handle_request(entorno, start_response)

Archivo "/usr/local/lib/python3.6/site-packages/socketio/server.py", línea 360, en handle_request
devolver self.eio.handle_request(entorno, start_response)

Archivo "/usr/local/lib/python3.6/site-packages/engineio/server.py", línea 279, en handle_request
enchufe = self._get_socket(sid)

Archivo "/usr/local/lib/python3.6/site-packages/engineio/server.py", línea 439, en _get_socket
aumentar KeyError('La sesión está desconectada')
`
Pero sospecho que puede tener algo que ver entre sí, ya que fuerzo que la conexión sea websocket, este error no se ha vuelto a ver.

Tener este problema también con gunicorn 19.9.0 y Flask-socketIO 3.0.2, cuando se usa eventlet 0.24.1

AttributeError: el objeto 'Respuesta' no tiene atributo 'status_code'

También experimenta este problema con los siguientes requisitos:

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

Mensaje de error al cerrar el navegador web que tiene una conexión de socket abierta:

 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'

Parece que este problema se ha solucionado en la última versión de python-engineio ..

He probado con la última versión de python-engineio (2.3.2), todavía no funciona.

¿Alguna novedad sobre este asunto? Recibo el mismo error cuando uso sentry-python

Tengo el mismo problema

evento: 0.25.1
matraz-socketio: 4.2.1
gunicornio: 19.9.0

image

image

como reproducirlo? cna usted proporciona un ejemplo simple?

Tampoco estoy seguro de cómo reproducirlo, pero ocurre A MENUDO cuando actualizo una página en mi aplicación gunicorn.

Encuentro el mismo problema, y ​​mi entorno es el mismo que @eazow mientras gunicorn == 20.0.4.
Parece que el problema ocurrió después de que instalé Sentry para el seguimiento de errores.
Los problemas pueden ser reproducidos por

  1. actualizar la página (no abrir una nueva página)
  2. cerrando la pagina

Curiosamente, abrir una nueva página no producirá el problema. No estoy seguro de por qué. ¡Gracias!

Tengo el mismo problema que @cowbonlin . La misma versión de gunicorn, también.

Después de instalar Sentry, estamos recibiendo cantidades increíbles de este error. Aunque me resulta difícil saber si esto siempre sucedió o no, ya que no rastreamos los errores antes de centinela.

Si bien no parece influir en la funcionalidad real de nuestro servidor, esto es solo una tonelada de spam.

Estamos viviendo lo mismo. Sentry instalado pero deshabilitado. ¿Algunas ideas?

Mismo problema con centinela instalado.

¿Tiene algún ejemplo que lo reproduzca sin centinela (deshabilitado o no)?

Además, en lugar de un espacio de nombres, presioné manualmente /api.

Además, en lugar de un espacio de nombres, presioné manualmente /api.

Qué significa eso ? ¿Este centinela está relacionado?

Además, en lugar de un espacio de nombres, presioné manualmente /api.

Qué significa eso ? ¿Este centinela está relacionado?

No, esto está relacionado con los espacios de nombres socket.io. Intenté eliminarlos e incluso después de eliminarlos, aparece el error. Sin embargo, recibo este otro error en la máquina local sin gunicorn o nginx, que puede estar relacionado.

Estos son mis requisitos:

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

Este es mi código matraz-socketio en el lado del servidor:

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

Y este es mi código React socket io en el lado del cliente:

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

Déjeme saber si esto ayuda. En Ubuntu, el error se parece al anterior y localmente en Windows se ve así:
```Rastreo (última llamada más reciente):
Archivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", línea 599, en handle_one_response
escribir (b'')
Archivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", línea 491, en escritura
aumentar AssertionError ("escribir () antes de start_response ()")
AssertionError: escribir() antes de start_response()

Durante el manejo de la excepción anterior, ocurrió otra excepción:

Rastreo (llamadas recientes más última):
Archivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", línea 357, en __init__
self.handle()
Archivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", línea 390, en handle
self.handle_one_request()
Archivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", línea 466, en handle_one_request
self.handle_one_response()
Archivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", línea 609, en handle_one_response
escribir (err_cuerpo)
Archivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", línea 538, en escritura
wfile.flush()
Archivo "C:\ProgramData\Anaconda3\lib\socket.py", línea 607, en escritura
devolver self._sock.send(b)
Archivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenio\base.py", línea 397, en envío
volver self._send_loop(self.fd.send, datos, banderas)
Archivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenio\base.py", línea 384, en _send_loop
devolver send_method(datos, *argumentos)
ConnectionAbortedError: [WinError 10053] El software de su máquina anfitriona canceló una conexión establecida

Durante el manejo de la excepción anterior, ocurrió otra excepción:

Rastreo (llamadas recientes más última):
Archivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\hubs\hub.py", línea 461, en fire_timers
Temporizador()
Archivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\hubs\timer.py", línea 59, en __call__
cb( argumentos, * kw)
Archivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\semaphore.py", línea 147, en _do_acquire
camarero.interruptor()
Archivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenthread.py", línea 221, en main
resultado = función (argumentos , * kwargs)
Archivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", línea 818, en process_request
proto.__init__(conn_state, self)
Archivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", línea 359, en __init__
auto.terminar()
Archivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", línea 732, final
BaseHTTPServer.BaseHTTPRequestHandler.finish(self)
Archivo "C:\ProgramData\Anaconda3\lib\socketserver.py", línea 784, en fin
self.wfile.close()
Archivo "C:\ProgramData\Anaconda3\lib\socket.py", línea 607, en escritura
devolver self._sock.send(b)
Archivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenio\base.py", línea 397, en envío
volver self._send_loop(self.fd.send, datos, banderas)
Archivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenio\base.py", línea 384, en _send_loop
devolver send_method(datos, *argumentos)
ConnectionAbortedError: [WinError 10053] El software de su máquina host canceló una conexión establecida```

Puedo confirmar que este error desaparece cuando Sentry está completamente deshabilitado. Sería genial si gunicorn fuera lo suficientemente robusto como para lidiar con esto.

Bump @benoitc

Puedo confirmar que este error desaparece cuando Sentry está completamente deshabilitado. Sería genial si gunicorn fuera lo suficientemente robusto como para lidiar con esto.

Descubrí que deshabilitar el FlaskIntegration de Sentry también hace que el error desaparezca.

Ver un comportamiento similar. El uso de New Relic en producción provoca este error con el matraz-socketio. En desarrollo, el middleware del depurador werkzeug debe cargarse antes de que se inicialice el matraz-socketio (por lo que no se aplica a la aplicación wsgi de engineio). El problema es que la producción es donde realmente no quiero que se disparen los errores.

No puedo reemplazar la respuesta en post_request de gunicorn config, pero traté de forzar un código de estado en resp.status_code. Aunque no tomó.

Este error se puede reproducir mediante el uso de FlaskIntegration de Sentry junto con Gunicorn y Flask-SocketIO. ¿Es posible solucionarlo pronto?

@Canicio pensamos intentarlo para deshacernos del error e incluso después de deshabilitar la integración, el error persiste.

¿Alguien tiene un código compartible/un ejemplo mínimo para que @benoitc se active?

Seguro:

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>

requisitos:

flask
sentry-sdk[flask]
flask-socketio
eventlet

ejemplo de configuración de gunicorn:

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

Al cargar / , se conectará al websocket. En la desconexión de websocket (por ejemplo, navegar lejos, actualizar), producirá una excepción como esta:

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

Nota: en realidad, nunca he usado centinela. Esto es solo de la página de inicio de Sentry. El ejemplo dsn funciona bien para nuestra prueba.

Comentar integrations=[FlaskIntegration()] eliminará el error (por supuesto, deshabilitará efectivamente a Sentry).

Por lo que vale, gevent-websocket se puede usar en lugar de eventlet sin errores. Sin embargo, luego parece manejar todas las solicitudes.

Ok, jugué un poco. Parece que Sentry/Newrelic envuelve la respuesta. Sin centinela, obtenemos <eventlet.wsgi._AlreadyHandled object at 0x7fd0f5b1c0d0> como se esperaba y EventletWorker.is_already_handled() de gunicorn detendrá la iteración.

Sin embargo, al usar centinela, esto se convierte en algo así como <sentry_sdk.integrations.wsgi._ScopedResponse object at 0x7f30155a5100> en su lugar, fallando la verificación

En su lugar, podríamos echar un vistazo al respirador para ver si está vacío. Mañana buscaré más.

Muy bien, aquí está la solución que se me ocurrió:

eventlet_fix.py:
ver editar a continuación

Y en mi gunicorn config.py: worker_class = 'eventlet_fix.EventletWorker .

El problema es que sentry/newrelic envuelve las respuestas, por lo que no podemos simplemente compararlo con ALREADY_HANDLED de eventlet. Dado que la naturaleza de una solicitud ya manejada es que no se llama a start_response de gunicorn, podemos verificar la presencia de un estado de respuesta.

Así que secuestré la llamada wsgi para luego verificar el estado de respuesta y piratear los valores de respuesta según sea necesario. Esto permite que gunicorn aún registre la solicitud. Si, por el contrario, se desea mantener el comportamiento original, se pueden generar StopIteration en su lugar.

Hackear el estado a 101 es apropiado para nuestro caso de uso aquí (flask-socketio websocket), pero de lo contrario, dejarlo como Ninguno también funciona, ya que headers_sent y should_close se fuerzan a Verdadero.

Nuevamente, esto supone que si status no está configurado, no se llamó a start_response y, por lo tanto, la solicitud debe haber sido "ya manejada" externamente.

editar: No es bueno. Habrá que reevaluar. Si la solicitud tarda en ejecutarse, no se llamará a start_response antes resp.status .

edit2: aquí hay una versión fija con un iterador de respuesta pirateado:

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)

Obviamente, esto es solo un parche de mono. La solución real podría incluir handle_request en base_async.py. La clave puede ser verificar (indirectamente) si se llamó a start_response después de iterar a través respiter , ya sea al marcar resp.status (solo start_response llamado) o resp.headers_sent (confirmación de que efectivamente hemos respondido a la solicitud).

@benoitc
@ziddey ha encontrado una manera de resolver el problema.

@ziddey preguntas rápidas para su ejemplo (ya que no estoy usando centinela).

  • ¿El error solo afecta al centinela o la solicitud también se detuvo, es decir, el trabajador está terminando (lo que sospecho que sucede si la respuesta está envuelta)?
  • ¿Espera tener algo envuelto allí o limpiar la solicitud incluso si la respuesta está envuelta estaría bien?

@benoitc no puede probar actualmente, pero mira el rastreo anterior https://github.com/benoitc/gunicorn/issues/1852#issuecomment -697189261 y https://github.com/benoitc/gunicorn/blob/4ae2a05c37b332773997f90ba7542713b9bf8274/ gunicorn/trabajadores/base_async.py#L107 -L140

Normalmente, is_already_handled devolvería True y terminaría aquí.

Sin embargo, debido a que la respuesta está envuelta, ese método no funciona. En cambio, la ejecución avanza y falla en la línea 115: resp.close() intenta enviar encabezados, pero nunca se llamó a start_response , por lo que no hay código de estado. Incluso si lo hiciera, obviamente fallaría en última instancia.

Esto da como resultado un AttributeError que se vuelve a generar y supuestamente es manejado por handle_error . Dado que la solicitud ya se manejó externamente, no hay ningún daño aquí aparte del spam de registro.

No puedo decir mucho sobre Sentry, tampoco lo estoy usando.

Sin embargo, un detalle: el mecanismo actual ya manejado da como resultado que no se registre el acceso. Supongo que técnicamente tiene sentido ya que no hay forma de saber cómo se manejó externamente. En mi respuesta pirateada, fuerzo el código de estado a 101, con headers_sent como Verdadero para que el controlador pueda continuar y la solicitud aún obtenga acceso registrado.

Marcar resp.status es una prueba definitiva para determinar si se start_response .

@benoitc revisando esto. Para concluir de manera más definitiva que la solicitud ya se manejó, environ['gunicorn.socket'] podría ser una especie de proxy para el objeto subyacente. De esa manera, se puede registrar cuando se accede directamente al socket (por ejemplo, envolviendo get_socket() para eventlet), y se puede usar para algo como is_already_handled

Sin embargo, aún requeriría piratear un estado de respuesta si se desea el registro de acceso.

¿Fue útil esta página
0 / 5 - 0 calificaciones