Gunicorn: Bug antigo reproduzido:O objeto 'Response' não tem atributo 'status_code' em wsgi.py com websockets

Criado em 9 ago. 2018  ·  33Comentários  ·  Fonte: benoitc/gunicorn

Assim como este antigo problema 1210 disse, gunicorn registra erro quando o cliente se desconecta e meu ambiente é:

  • Debian GNU/Linux 7.8

  • nginx

  • Python3.4

  • gunicorn(19.8.1) (com um ou vários trabalhadores)

  • Flask-SocketIO, cliente especifica o transporte de websocket

Tudo funciona bem, incluindo clientes, exceto por este log de erros, duas instâncias de produção independentes da nuvem, ambas registradas persistentemente, mas não consigo reproduzi-lo na minha máquina de desenvolvimento, que é um mac.

Muito obrigado pela sua ajuda.

Erro ao processar a solicitação /socket.io/?EIO=3&transport=websocket
Traceback (última chamada mais recente):
Arquivo "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/workers/async.py", linha 56, em handle
self.handle_request(listener_name, req, client, addr)
Arquivo "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/workers/async.py", linha 116, em handle_request
resp.close()
Arquivo "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py", linha 409, em close
self.send_headers()
Arquivo "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py", linha 325, em send_headers
tosend = self.default_headers()
Arquivo "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py", linha 306, em default_headers
elif self.should_close():
Arquivo "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py", linha 229, em should_close
se self.status_code < 200 ou self.status_code em (204, 304):
AttributeError: objeto 'Response' não tem atributo 'status_code'

Feedback Requested unconfirmed ThirdPartFlask

Comentários muito úteis

Bump @benoitc

Todos 33 comentários

você tem algum exemplo simples para reproduzi-lo? Além disso, tente com o mestre mais recente, se possível.

Anteriormente, tentei várias vezes no meu ambiente de desenvolvimento local, que é o mesmo código do aplicativo com o ambiente de produção, mas não consigo reproduzi-lo.

E eu verifiquei o log de lançamento da versão 19.9.0, não encontrei nada relacionado, vou manter
observando este log de erros, se encontrar algo novo, eu postaria aqui.

Eu também tenho esse problema, especificamente quando forço toda a minha conexão do cliente ao protocolo websocket. Minhas configurações são as mesmas do BoWuGit. Se permitir o protocolo de pesquisa antes da atualização, isso não aparece, mas outro erro:
`
[ERRO] Erro ao processar a solicitação /socket.io/?EIO=3&transport=polling&t=MPRHUoV&sid=cd64be7c940e474d8728b114c3fb9bbe

Traceback (última chamada mais recente):
Arquivo "/usr/local/lib/python3.6/site-packages/gunicorn/workers/async.py", linha 56, em handle
self.handle_request(listener_name, req, client, addr)

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

Arquivo "/usr/local/lib/python3.6/site-packages/flask/app.py", linha 1994, em __call__
return self.wsgi_app(environ, start_response)

Arquivo "/usr/local/lib/python3.6/site-packages/flask_socketio/__init__.py", linha 43, em __call__
start_response)

Arquivo "/usr/local/lib/python3.6/site-packages/engineio/middleware.
py", linha 47, em __call__
return self.engineio_app.handle_request(environ, start_response)

Arquivo "/usr/local/lib/python3.6/site-packages/socketio/server.py", linha 360, em handle_request
return self.eio.handle_request(environ, start_response)

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

Arquivo "/usr/local/lib/python3.6/site-packages/engineio/server.py", linha 439, em _get_socket
raise KeyError('Sessão desconectada')
`
Mas suspeito que possa ter algo a ver um com o outro, já que forcei a conexão a ser websocket, esse erro não foi visto novamente.

Tendo esse problema também com gunicorn 19.9.0 e Flask-socketIO 3.0.2, ao usar o eventlet 0.24.1

AttributeError: objeto 'Response' não tem atributo 'status_code'

Também enfrentando esse problema com os seguintes requisitos:

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

Mensagem de erro ao fechar o navegador da Web com conexão de soquete aberta:

 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 esse problema foi corrigido na versão mais recente do python-engineio ..

Testei com a versão mais recente do python-engineio (2.3.2), ainda não funciona.

Alguma novidade sobre este assunto? Eu recebo o mesmo erro ao usar sentry-python

Eu tenho o mesmo problema

eventlet: 0,25,1
frasco-soquete: 4.2.1
gunicórnio: 19.9.0

image

image

como reproduzi-lo? você pode fornecer um exemplo simples?

também não sei como reproduzi-lo, mas acontece FREQUENTEMENTE quando atualizo uma página no meu aplicativo gunicorn

Encontre o mesmo problema e meu ambiente é o mesmo que @eazow enquanto gunicorn == 20.0.4.
Parece que o problema aconteceu depois que instalei o sentry para rastreamento de bugs.
Os problemas podem ser reproduzidos por

  1. atualizando a página (não abrindo uma nova página)
  2. fechando a página

Curiosamente, abrir uma nova página não produzirá o problema. Não tenho certeza por quê. Obrigado!

Eu tenho o mesmo problema que @cowbonlin . A mesma versão do gunicorn também.

Depois de instalar o sentry, estamos recebendo quantidades loucas desse erro. Embora eu ache difícil dizer se isso sempre aconteceu ou não - já que não rastreamos erros antes da sentinela.

Embora não pareça influenciar a funcionalidade real do nosso servidor, isso é apenas uma tonelada de spam.

Estamos vivenciando o mesmo. Sentinela instalado mas desabilitado. Alguma ideia?

Mesmo problema com o sentinela instalado.

Você tem algum exemplo que o reproduza sem sentry (desativado ou não)?

Além disso, em vez de um namespace, pressiono manualmente /api.

Além disso, em vez de um namespace, pressiono manualmente /api.

O que isso significa ? Esta sentinela está relacionada?

Além disso, em vez de um namespace, pressiono manualmente /api.

O que isso significa ? Esta sentinela está relacionada?

Não, isso está relacionado aos namespaces socket.io. Tentei removê-los e, mesmo depois de removê-los, recebo o erro. No entanto, recebo esse outro erro na máquina local sem gunicorn ou nginx, que pode estar relacionado.

Estes são os meus requisitos:

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

Este é o meu código flask-socketio no lado do servidor:

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

E este é o meu código React socket io no lado do 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
          });

Avise-me se isso ajudar. No Ubuntu, o erro se parece com o acima e, localmente, no Windows, é assim:
```Traceback (última chamada mais recente):
Arquivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", linha 599, em handle_one_response
escreva(b'')
Arquivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", linha 491, em gravação
raise AssertionError("write() antes de start_response()")
AssertionError: write() antes de start_response()

Durante o tratamento da exceção acima, ocorreu outra exceção:

Traceback (última chamada mais recente):
Arquivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", linha 357, em __init__
self.handle()
Arquivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", linha 390, no handle
self.handle_one_request()
Arquivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", linha 466, em handle_one_request
self.handle_one_response()
Arquivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", linha 609, em handle_one_response
escreva(err_body)
Arquivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", linha 538, na gravação
wfile.flush()
Arquivo "C:\ProgramData\Anaconda3\lib\socket.py", linha 607, na escrita
return self._sock.send(b)
Arquivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenio\base.py", linha 397, em enviar
return self._send_loop(self.fd.send, data, flags)
Arquivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenio\base.py", linha 384, em _send_loop
return send_method(dados, *args)
ConnectionAbortedError: [WinError 10053] Uma conexão estabelecida foi abortada pelo software em sua máquina host

Durante o tratamento da exceção acima, ocorreu outra exceção:

Traceback (última chamada mais recente):
Arquivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\hubs\hub.py", linha 461, em fire_timers
cronômetro()
Arquivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\hubs\timer.py", linha 59, em __call__
cb( args, * kw)
Arquivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\semaphore.py", linha 147, em _do_acquire
garçom.interruptor()
Arquivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenthread.py", linha 221, em main
resultado = função( args, * kwargs)
Arquivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", linha 818, em process_request
proto.__init__(conn_state, self)
Arquivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", linha 359, em __init__
self.finish()
Arquivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", linha 732, em acabamento
BaseHTTPServer.BaseHTTPRequestHandler.finish(self)
Arquivo "C:\ProgramData\Anaconda3\lib\socketserver.py", linha 784, em acabamento
self.wfile.close()
Arquivo "C:\ProgramData\Anaconda3\lib\socket.py", linha 607, na escrita
return self._sock.send(b)
Arquivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenio\base.py", linha 397, em enviar
return self._send_loop(self.fd.send, data, flags)
Arquivo "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenio\base.py", linha 384, em _send_loop
return send_method(dados, *args)
ConnectionAbortedError: [WinError 10053] Uma conexão estabelecida foi abortada pelo software em sua máquina host```

Pode confirmar que este erro desaparece quando a sentinela está totalmente desabilitada. Seria ótimo se o gunicorn fosse robusto o suficiente para lidar com isso.

Bump @benoitc

Pode confirmar que este erro desaparece quando a sentinela está totalmente desabilitada. Seria ótimo se o gunicorn fosse robusto o suficiente para lidar com isso.

Descobri que desabilitar o FlaskIntegration do Sentinela também faz o erro desaparecer.

Vendo comportamento semelhante. Usar o New Relic em produção causa esse erro com o flask-socketio. Em desenvolvimento, o middleware do depurador werkzeug precisa ser carregado antes que o flask-socketio seja inicializado (portanto, não é aplicado ao aplicativo wsgi do engineio). O problema é que a produção é onde eu realmente não quero que os erros tropecem.

Não é possível substituir a resposta no post_request da configuração do gunicorn, mas tentei forçar um código de status em resp.status_code. Mas não demorou.

Este erro é reproduzível usando FlaskIntegration do Sentry junto com Gunicorn e Flask-SocketIO. É possível resolvê-lo em breve?

@Canicio pensamos em tentar isso para se livrar do erro e mesmo depois de desabilitar a integração, o erro persiste.

Alguém tem código compartilhável/um exemplo mínimo para @benoitc sair?

Certo:

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

exemplo de configuração do gunicorn:

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

Ao carregar / ele se conectará ao websocket. Na desconexão do websocket (por exemplo, navegar, atualizar), produzirá uma exceção assim:

[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: Eu nunca usei sentinela. Isso é apenas da página de introdução da sentinela . O exemplo dsn funciona bem para o nosso teste.

Comentar integrations=[FlaskIntegration()] irá então eliminar o erro (é claro que desabilita efetivamente o sentry).

Por que vale a pena, gevent-websocket pode ser usado em vez de eventlet sem erros. No entanto, parece lidar com todas as solicitações.

Ok, fiz algumas brincadeiras. Parece que sentry/newrelic envolve a resposta. Sem sentry, obtemos <eventlet.wsgi._AlreadyHandled object at 0x7fd0f5b1c0d0> como esperado e EventletWorker.is_already_handled() do gunicorn parará a iteração.

No entanto, ao usar sentry, isso se torna algo como <sentry_sdk.integrations.wsgi._ScopedResponse object at 0x7f30155a5100> , falhando na verificação

Em vez disso, poderíamos espiar o respiter para ver se está vazio. Vai olhar mais amanhã.

Tudo bem, aqui está a solução alternativa que eu criei:

eventlet_fix.py:
veja a edição abaixo

E no meu gunicorn config.py: worker_class = 'eventlet_fix.EventletWorker .

O problema é que sentry/newrelic envolve as respostas, então não podemos simplesmente verificá-las com ALREADY_HANDLED do eventlet. Como a natureza de uma solicitação já tratada é que o start_response do gunicorn não é chamado, podemos verificar a presença de um status de resposta.

Então, eu sequestrei a chamada wsgi para verificar um status de resposta e hackear valores de resposta conforme necessário. Isso permite que a solicitação ainda seja registrada pelo gunicorn. Se, em vez disso, for desejado manter o comportamento original, StopIteration pode ser aumentado.

Hackear o status para 101 é apropriado para nosso caso de uso aqui (flask-socketio websocket), mas, caso contrário, deixá-lo como None também funciona, pois headers_sent e should_close são forçados a True.

Novamente, isso pressupõe que se status não estiver definido, start_response não foi chamado e, portanto, a solicitação deve ter sido "já tratada" externamente.

editado: Não é bom. Precisará reavaliar. Se a solicitação demorar para ser executada, start_response não será chamado antes que resp.status seja verificado.

edit2: Aqui está uma versão corrigida com um iterador de resposta hackeado:

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 este é apenas um patch de macaco. A correção real poderia ir em handle_request em base_async.py. A chave pode ser (indiretamente) verificar se start_response foi chamado após iterar por respiter , seja verificando resp.status (apenas start_response chamado) ou resp.headers_sent (confirmação de que realmente respondemos à solicitação).

@benoitc
@ziddey encontrou uma maneira de resolver o problema.

@ziddey perguntas rápidas para o seu exemplo (já que não estou usando sentry).

  • O erro está afetando apenas o sentry ou a solicitação também está parada, ou seja, o trabalhador está terminando (o que eu suspeito que aconteça se a resposta for encapsulada)?
  • você espera ter algo embrulhado lá ou limpar a solicitação, mesmo que a resposta seja embrulhada, estaria bem?

@benoitc não pode testar atualmente, mas olhando para o traceback acima https://github.com/benoitc/gunicorn/issues/1852#issuecomment -697189261 e https://github.com/benoitc/gunicorn/blob/4ae2a05c37b332773997f90ba7542713b9bf8274/ gunicorn/workers/base_async.py#L107 -L140

Normalmente, is_already_handled retornaria True e terminaria aqui.

No entanto, como a resposta é encapsulada, esse método não funciona. Em vez disso, a execução progride, falhando na linha 115: resp.close() tenta enviar cabeçalhos, mas start_response nunca foi chamado, então não há código de status. Mesmo que isso acontecesse, ainda assim, obviamente, falharia.

Isso resulta em um AttributeError que é re-aumentado e supostamente tratado por handle_error . Como a solicitação já foi tratada externamente, não há nenhum mal aqui além de spam de log.

Não posso falar muito sobre o Sentinela - também não o estou usando.

Um detalhe, porém: o mecanismo atual já manipulado resulta em nenhum registro de acesso. Suponho que isso tecnicamente faz sentido, pois não há como saber como isso foi tratado externamente. Na minha resposta hackeada, forço o código de status para 101, com headers_sent como True para que o manipulador possa prosseguir e a solicitação ainda tenha acesso registrado.

Verificar resp.status é um teste definitivo para determinar se start_response foi chamado.

@benoitc revisitando isso. Para concluir mais definitivamente que a solicitação já foi tratada, environ['gunicorn.socket'] pode ser algum tipo de proxy para o objeto subjacente. Dessa forma, ele pode ser gravado quando o soquete é acessado diretamente (por exemplo, envolvendo get_socket() para eventlet) e usado para algo como is_already_handled

Ainda seria necessário hackear um status de resposta se o log de acesso for desejado.

Esta página foi útil?
0 / 5 - 0 avaliações