Gunicorn: Воспроизведена старая ошибка: объект «Response» не имеет атрибута «status_code» в wsgi.py с веб-сокетами.

Созданный на 9 авг. 2018  ·  33Комментарии  ·  Источник: benoitc/gunicorn

Как и в этой старой проблеме 1210 , ошибка журнала gunicorn при отключении клиента, и моя среда:

  • Дебиан GNU/Linux 7.8

  • нгинкс

  • Python3.4

  • gunicorn(19.8.1) (с одним или несколькими рабочими)

  • Flask-SocketIO, клиент указывает транспорт веб-сокета

Все работает хорошо, включая клиентов, за исключением этого журнала ошибок, двух независимых облачных производственных экземпляров, которые постоянно регистрируются, но я не могу воспроизвести его на своей машине разработки, которая является Mac.

Большое спасибо за вашу помощь.

Ошибка обработки запроса /socket.io/?EIO=3&transport=websocket
Traceback (последний последний вызов):
Файл "/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
соответственно закрыть()
Файл "/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
отправить = self.default_headers()
Файл "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py", строка 306, в default_headers
Элиф 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: объект «Ответ» не имеет атрибута «status_code»

Feedback Requested unconfirmed ThirdPartFlask

Самый полезный комментарий

Бамп @benoitc

Все 33 Комментарий

у вас есть простой пример, чтобы воспроизвести его? Также, пожалуйста, попробуйте с последним мастером, если это возможно.

Раньше я несколько раз пробовал в своей локальной среде разработки, которая представляет собой тот же код приложения, что и в производственной среде, но я не могу воспроизвести его.

И я проверил журнал выпуска версии 19.9.0,не нашел ничего связанного,сохраню
наблюдая за этим журналом ошибок, если найду что-то новое, я бы разместил здесь.

У меня тоже есть эта проблема, особенно когда я принудительно подключаюсь от клиента к протоколу веб-сокета. Мои настройки такие же, как у BoWuGit. Если разрешить протокол опроса перед обновлением, это не отображается, а появляется другая ошибка:
`
[ОШИБКА] Ошибка обработки запроса /socket.io/?EIO=3&transport=polling&t=MPRHUoV&sid=cd64be7c940e474d8728b114c3fb9bbe

Traceback (последний последний вызов):
Файл "/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)

Файл "/usr/local/lib/python3.6/site-packages/flask/app.py", строка 1994, в __call__
вернуть self.wsgi_app (среда, start_response)

Файл "/usr/local/lib/python3.6/site-packages/flask_socketio/__init__.py", строка 43, в __call__
старт_ответ)

Файл "/usr/local/lib/python3.6/site-packages/engineio/middleware.
ру", строка 47, в __call__
вернуть self.engineio_app.handle_request (среда, 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 (сид)

Файл "/usr/local/lib/python3.6/site-packages/engineio/server.py", строка 439, в _get_socket
поднять KeyError('Сеанс отключен')
`
Но я подозреваю, что это может иметь какое-то отношение друг к другу, поскольку я заставляю соединение быть веб-сокетом, эта ошибка больше не наблюдалась.

Эта проблема также возникает с gunicorn 19.9.0 и Flask-socketIO 3.0.2 при использовании eventlet 0.24.1.

AttributeError: объект «Ответ» не имеет атрибута «status_code»

Также возникает эта проблема со следующими требованиями:

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

Сообщение об ошибке при закрытии веб-браузера с открытым сокетом:

 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
колба-сокетио: 4.2.1
стрелорог: 19.9.0

image

image

как это воспроизвести? cna вы приводите простой пример?

Я также не уверен, как это воспроизвести, но это происходит ЧАСТО, когда я обновляю страницу в своем приложении Gunicorn.

Столкнулся с той же проблемой, и моя среда такая же, как @eazow , а gunicorn == 20.0.4.
Похоже, проблема возникла после того, как я установил sentry для отслеживания ошибок.
Проблемы могут быть воспроизведены с помощью

  1. обновление страницы (не открытие новой страницы)
  2. закрытие страницы

Интересно, что открытие новой страницы не вызовет проблемы. Не уверен, почему. Спасибо!

У меня та же проблема, что и у @cowbonlin . Та же версия стрелкового оружия.

После установки sentry мы получаем сумасшедшее количество этой ошибки. Хотя мне сложно сказать, всегда ли так было или нет, так как мы не отслеживали ошибки перед часовым.

Хотя это не влияет на реальную функциональность нашего сервера, это просто куча спама.

Мы переживаем то же самое. Sentry установлен, но отключен. Любые идеи?

Та же проблема с установленным 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 на стороне клиента:

          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
написать (б'')
Файл "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", строка 491, запись
поднять AssertionError("записать() перед start_response()")
AssertionError: запись() перед start_response()

Во время обработки вышеупомянутого исключения произошло другое исключение:

Traceback (последний последний вызов):
Файл "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, при отправке
вернуть self._send_loop(self.fd.send, данные, флаги)
Файл "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenio\base.py", строка 384, в _send_loop
вернуть send_method(данные, *аргументы)
ConnectionAbortedError: [WinError 10053] Установленное соединение было прервано программным обеспечением на вашем хост-компьютере.

Во время обработки вышеупомянутого исключения произошло другое исключение:

Traceback (последний последний вызов):
Файл "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( аргументы, * кВт)
Файл "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\semaphore.py", строка 147, в _do_acquire
официант.переключатель()
Файл "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenthread.py", строка 221, в основном
результат = функция ( аргументы, * 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__
самостоятельная отделка ()
Файл "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", строка 732, готово
BaseHTTPServer.BaseHTTPRequestHandler.finish(сам)
Файл "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, данные, флаги)
Файл "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenio\base.py", строка 384, в _send_loop
вернуть send_method(данные, *аргументы)
ConnectionAbortedError: [WinError 10053] Установленное соединение было прервано программным обеспечением на вашем хост-компьютере```

Могу подтвердить, что эта ошибка исчезает, когда часовой полностью отключен. Было бы здорово, если бы стрелокорн был достаточно силен, чтобы справиться с этим.

Бамп @benoitc

Могу подтвердить, что эта ошибка исчезает, когда часовой полностью отключен. Было бы здорово, если бы стрелокорн был достаточно силен, чтобы справиться с этим.

Я обнаружил, что отключение FlaskIntegration Sentry также приводит к исчезновению ошибки.

Наблюдение за подобным поведением. Использование New Relic в производственной среде вызывает эту ошибку с flask-socketio. В процессе разработки перед инициализацией flask-socketio необходимо загрузить промежуточное программное обеспечение отладчика werkzeug (поэтому оно не применяется к приложению wsgi от engineio). Проблема в том, что производство — это то, где я действительно не хочу, чтобы ошибки отключались.

Не могу заменить ответ в 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

пример конфига пушкикорна:

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

При загрузке / он подключится к веб-сокету. При отключении веб-сокета (например, уходе, обновлении) будет создаваться такое исключение:

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

Примечание. Сам я никогда не использовал sentry. Это только со страницы начала работы часового. Пример dsn отлично подходит для нашего теста.

Комментарий integrations=[FlaskIntegration()] устранит ошибку (конечно, эффективно отключив часовой).

Что бы это ни стоило, gevent-websocket можно использовать вместо eventlet без ошибок. Однако, похоже, он обрабатывает все запросы.

Ладно, поигрался. Похоже, что sentry/newrelic оборачивает ответ. Без часового мы получаем <eventlet.wsgi._AlreadyHandled object at 0x7fd0f5b1c0d0> , как и ожидалось, и EventletWorker.is_already_handled() пушкирога остановит итерацию.

Однако при использовании часового это становится чем-то вроде <sentry_sdk.integrations.wsgi._ScopedResponse object at 0x7f30155a5100> , что приводит к сбою проверки.

Вместо этого мы могли бы заглянуть в respiter, чтобы убедиться, что он пуст. Завтра посмотрю дальше.

Хорошо, вот обходной путь, который я придумал:

eventlet_fix.py:
см. правку ниже

И в моем config.py пушки: worker_class = 'eventlet_fix.EventletWorker .

Проблема в том, что sentry/newrelic оборачивает ответы, поэтому мы не можем просто проверить их на ALREADY_HANDLED eventlet'а. Поскольку природа уже обработанного запроса такова, что start_response пушки не вызывается, вместо этого мы можем проверить наличие статуса ответа.

Поэтому я перехватил вызов wsgi, чтобы затем проверить статус ответа и при необходимости взломать значения ответа. Это позволяет пушке по-прежнему регистрировать запрос. Если вместо этого желательно сохранить исходное поведение, вместо этого можно поднять StopIteration .

Взлом статуса до 101 подходит для нашего варианта использования здесь (веб-сокет flask-socketio), но в остальном, оставляя его равным None, также работает, поскольку headers_sent и should_close принудительно устанавливаются в True.

Опять же, это предполагает, что если status не установлено, start_response не вызывалось, и поэтому запрос должен быть "уже обработан" извне.

редактировать: нехорошо. Нужно будет переоценить. Если для выполнения запроса требуется время, start_response не будет вызываться до тех пор, пока не будет проверено resp.status .

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)

Очевидно, это всего лишь обезьяний патч. Фактическое исправление потенциально может находиться в handle_request в base_async.py. Ключ может состоять в том, чтобы (косвенно) проверить, был ли вызван 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/ пистолет/рабочие/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 рейтинги