Вышел Gunicorn 27.0: сервер строже проверяет запросы, а ASGI-воркер стал быстрее

10 ноября разработчики Gunicorn выпустили версию 27.0.0. Gunicorn это сервер, на котором в продакшене обычно крутятся Django, Flask и FastAPI-приложения, как правило за nginx. Новых функций в релизе почти нет. Почти все изменения про одно: сервер теперь гораздо строже следит, чтобы запросы и ответы соответствовали стандартам HTTP, а ASGI-воркер заодно стал быстрее.

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

Что сломается: запросы с Transfer-Encoding: identity

Заголовок Transfer-Encoding сообщает серверу, как передаётся тело запроса. По стандарту HTTP/1.1 (RFC 9112, раздел 6.3) последним в нём должно стоять chunked, иначе сервер не может надёжно понять, где кончается тело, и обязан ответить ошибкой 400. Gunicorn 27 теперь так и делает: запрос, где указано только gzip или identity, получит 400, даже если рядом есть Content-Length.

Раньше такие запросы обрабатывались по-разному в зависимости от воркера. WSGI-воркеры читали тело, пока клиент не закроет соединение, а ASGI-воркер считал тело пустым. Когда сервер и прокси перед ним по-разному понимают границы запроса, это открывает дорогу атакам с подменой запросов (HTTP request smuggling), поэтому стандарт и требует такие запросы отклонять.

Браузеры и обычные HTTP-библиотеки так не пишут. Под удар попадают старые скрипты, интеграции и устройства, которые зачем-то явно отправляют Transfer-Encoding: identity. Разработчики Gunicorn просят такие клиенты исправить. Проверить, как сервер отвечает на такой запрос, можно прямо на машине с приложением (порт подставьте свой):

curl -i -X POST http://127.0.0.1:8000/ -H 'Transfer-Encoding: identity' -d 'test'
# после обновления до 27.0.0 ответ будет HTTP/1.1 400 Bad Request

Какие ещё запросы теперь получат 400

В списке исправлений несколько десятков пунктов, и большинство из них про то же: Gunicorn перестаёт прощать запросы, которые нарушают стандарт. Теперь сервер отклоняет:

  • пробел между именем заголовка и двоеточием, например Host :;
  • Content-Length, в котором есть что-то кроме цифр;
  • управляющие символы в адресе и русские буквы или другие не-ASCII символы, не закодированные через %;
  • строки, которые заканчиваются одним переводом строки без возврата каретки: раньше на таком запросе соединение могло висеть до таймаута, теперь ответ 400 приходит сразу.

Слишком длинная строка запроса, больше limit_request_line (по умолчанию 4094 байта), теперь получает не 400, а более честный ответ 414 URI Too Long. После обновления стоит заглянуть в логи и посмотреть, не стало ли больше ответов 400 и 414: так быстрее всего найти клиента, который до сих пор жил на нестандартных запросах.

Есть изменение и для тех, кто запускает приложение через ASGI-воркер за прокси. Настройка forwarded_allow_ips теперь работает в нём и для HTTP/1, как давно работала в WSGI. Заголовки вроде X-Forwarded-Proto, по которым приложение понимает, что пользователь пришёл по HTTPS, принимаются только от доверенных адресов, а по умолчанию это 127.0.0.1 и ::1. Если nginx стоит на другой машине или в соседнем контейнере, пропишите его адрес в forwarded_allow_ips, иначе приложение может решить, что запрос пришёл по HTTP, и начать бесконечно редиректить на HTTPS.

ASGI-воркер стал быстрее

ASGI-воркер нужен для асинхронных приложений, например на FastAPI или Starlette. В 27.0 он делает меньше работы на каждый запрос: настройки и список доверенных прокси читаются один раз на соединение, заголовки разбираются за один проход, а ответы от 64 КБ и больше отправляются без лишнего копирования в памяти. По замерам разработчиков, с одним воркером небольшие запросы обрабатываются примерно на треть быстрее, потоковые ответы больше чем в полтора раза быстрее, а большие ответы под нагрузкой занимают заметно меньше памяти.

Ещё несколько полезных исправлений в том же воркере:

  • если приложение сообщает, что не смогло запуститься (ошибка на этапе lifespan startup), Gunicorn останавливается, а не перезапускает воркер по кругу и не принимает запросы;
  • простаивающие keep-alive соединения закрываются через keepalive секунд, как в остальных воркерах;
  • каждый запрос получает свою копию scope["state"], и значение, которое сохранил один запрос, больше не видно следующему;
  • на Expect: 100-continue сервер отвечает сразу, поэтому curl и другие клиенты больше не ждут около секунды перед отправкой файла;
  • у WebSocket появился лимит на размер сообщения websocket_max_message_size, по умолчанию 16 МБ. Сообщение больше закрывает соединение с кодом 1009, а 0 снимает ограничение.

Загрузка файлов и перезапуски воркеров

Есть приятные изменения и для классических WSGI-проектов. Тело запроса известной длины Gunicorn теперь читает большими кусками, а не по 8 КБ, как раньше. Загрузка больших файлов от этого заметно ускоряется, особенно когда другие потоки нагружают воркер. Так же теперь читаются и тела, переданные частями (chunked).

В воркере gthread простаивающие keep-alive соединения больше не задерживают плавную остановку. Пока воркер ждёт, он продолжает отчитываться главному процессу, поэтому его не убивают по таймауту во время плановых перезапусков по max_requests. Для тех, кто запускает Gunicorn через systemd socket activation, сервер больше не вызывает listen() повторно на уже готовом сокете: SELinux мог такой вызов запретить.

Для HTTP/2 добавили защиту от потока служебных кадров: если клиент присылает больше 1000 кадров PING, SETTINGS или пустых DATA за 10 секунд, соединение закрывается. Метрики StatsD теперь уходят и на IPv6-адрес, раньше они молча терялись.

Как обновиться

Gunicorn 27.0.0 уже на PyPI и требует Python 3.10 или новее. Обновить его в виртуальном окружении проекта и перезапустить сервис можно так (имя юнита systemd подставьте своё):

python -m pip install --upgrade "gunicorn==27.0.0"
# если ставите быстрый парсер, он подтянет gunicorn_h1c 0.6.14
python -m pip install --upgrade "gunicorn[fast]==27.0.0"
gunicorn --version
sudo systemctl restart myapp.service

Gunicorn 27.0 стоит поставить всем, кто держит Python-приложения на своём сервере: он строже отсекает кривые запросы, на которых строятся атаки через прокси, а асинхронные проекты получат ещё и прирост скорости. Перед обновлением проверьте, не шлют ли ваши интеграции Transfer-Encoding: identity, а в ASGI-режиме за удалённым прокси пропишите forwarded_allow_ips. После выкладки пару дней посмотрите в логах на ответы 400 и 414.

Источники и ссылки


Комментарии загружаются…