1 post tagged

go

Go HTTP-client. Баг или фитча?

В нашем арсенале имеется небольшой сервис, задачей которого является сбор новостей из RSS-лент с нотификацией в Telegram и все было отлично до обновления Go с версии 1.5.1 до 1.8.1. После обновления одна из пары сотен лент перестала работать, если быть точным, то лента “Twitter Engineering Blog” (RSS).

Очевидно, что могла быть ошибка где-то со стороны самого кода, поэтому проблемная область была ограничена до такого вот куска кода:

С помощью стандартного HTTP-клиента делаем GET-запрос, выводим в консоль то, что было отправлено в запросе и то, что вернулось в ответ.

HTTP-клиент по умолчанию использует стандартный транспорт, который поддерживает gzip-сжатие, работает это так – транспорт, если не установлено `DisableCompression=true` и если пользователь сам не задал `Accept-Encoding`, в каждый запрос добавляет заголовок `Accept-Encoding: gzip`, если в заголовке ответа есть `Content-Encoding: gzip`, то транспорт распаковывает содержимое и удаляет из ответа `Content-Encoding`. Для клиента это выглядит как обычный запрос без сжатия.

Наличие заголовка `Accept-Encoding` не задает жесткого ограничения на ответ – это рекомендация, заголовок определяет, какие типы сжатия и настройки поддерживаются клиентом, тем самым указывая gzip и/или deflate мы должны учитывать, что контент может быть как сжат одним из этих алгоритмов, так и быть без сжатия. Передача ответа в сжатом виде, неподдерживаемом клиентом, возможна, но нарушает принцип работы и назначение самого заголовка `Accept-Encoding`.

И тут мы подходим напрямую к проблеме – при стандартном запросе (с указанием поддержки gzip), получаем контент сжатый алгоритмом Deflate, естественно, транспорт такой ответ не распаковывает, получаем тело ответа, с которым нельзя начать работать без распаковки. Кажется, что решить такую проблему можно проверкой значения `Content-Encoding` и распаковкой тела, но ведь это не решает корень проблемы – клиент не заявлял поддержку этого алгоритма сжатия, а значит, не должен его получать.

Первое, что приходит в голову – ошибка на стороне самого сервиса, например, он неверно трактует заголовок запроса и всегда отдает контент сжатый Deflate.

Проверяем gzip:

curl https://blog.twitter.com/api/blog.rss?name=engineering -sH "Accept-Encoding: gzip" -D - | grep encoding
content-encoding: gzip

Проверяем deflate:

curl https://blog.twitter.com/api/blog.rss?name=engineering -sH "Accept-Encoding: deflate" -D - | grep encoding
content-encoding: deflate

Проверяем identity (без сжатия):

curl https://blog.twitter.com/api/blog.rss?name=engineering -sH "Accept-Encoding: identity" -D - | grep encoding
<?xml version="1.0" encoding="utf-8"?>

Проверяем без передачи заголовка `Accept-Encoding` – это трактуется как отсутствие поддержки каких-либо алгоритмов сжатия:

curl https://blog.twitter.com/api/blog.rss?name=engineering -s -D - | grep encoding
<?xml version="1.0" encoding="utf-8"?>

Никаких противоречий нет. Проверим, как менялось поведения транспорта и самого HTTP-клиента от версии к версии. Для этого я написал небольшой скрипт:

В результате получаем:

1.4.3-alpine: Content-Encoding:
1.5.1-alpine: Content-Encoding:
1.5.2-alpine: Content-Encoding:
1.5.3-alpine: Content-Encoding:
1.5.4-alpine: Content-Encoding:
1.6.0-alpine: Content-Encoding:
1.6.1-alpine: Content-Encoding:
1.6.2-alpine: Content-Encoding: deflate
1.6.3-alpine: Content-Encoding: deflate
1.6.4-alpine: Content-Encoding: deflate
1.7.0-alpine: Content-Encoding: deflate
1.7.1-alpine: Content-Encoding: deflate
1.7.3-alpine: Content-Encoding: deflate
1.7.4-alpine: Content-Encoding: deflate
1.7.5-alpine: Content-Encoding: deflate
1.8.0-alpine: Content-Encoding: deflate
1.8.1-alpine: Content-Encoding: deflate

До версии 1.6.2 транспорт запрашивал gzip и распаковывал без каких-либо проблем. Отключим в транспорте поддержку сжатия и проведем тест еще раз:

1.4.3-alpine: Content-Encoding: gzip
1.5.1-alpine: Content-Encoding: gzip
1.5.2-alpine: Content-Encoding: gzip
1.5.3-alpine: Content-Encoding: gzip
1.5.4-alpine: Content-Encoding: gzip
1.6.0-alpine: Content-Encoding: gzip
1.6.1-alpine: Content-Encoding: gzip
1.6.2-alpine: Content-Encoding: deflate
1.6.3-alpine: Content-Encoding: deflate
1.6.4-alpine: Content-Encoding: deflate
1.7.0-alpine: Content-Encoding: deflate
1.7.1-alpine: Content-Encoding: deflate
1.7.3-alpine: Content-Encoding: deflate
1.7.4-alpine: Content-Encoding: deflate
1.7.5-alpine: Content-Encoding: deflate
1.8.0-alpine: Content-Encoding: deflate
1.8.1-alpine: Content-Encoding: deflate

Запросим контент без сжатия:

1.4.3-alpine: Content-Encoding:
1.5.1-alpine: Content-Encoding:
1.5.2-alpine: Content-Encoding:
1.5.3-alpine: Content-Encoding:
1.5.4-alpine: Content-Encoding:
1.6.0-alpine: Content-Encoding:
1.6.1-alpine: Content-Encoding:
1.6.2-alpine: Content-Encoding: deflate
1.6.3-alpine: Content-Encoding: deflate
1.6.4-alpine: Content-Encoding: deflate
1.7.0-alpine: Content-Encoding: deflate
1.7.1-alpine: Content-Encoding: deflate
1.7.3-alpine: Content-Encoding: deflate
1.7.4-alpine: Content-Encoding: deflate
1.7.5-alpine: Content-Encoding: deflate
1.8.0-alpine: Content-Encoding: deflate
1.8.1-alpine: Content-Encoding: deflate

Проблема явно появилась с версии 1.6.2, хорошо, что между этой и предыдущей версией произошло не так много изменений. Благодаря зоркому глазу Владимиру Смирнову из чата @pro.go, удалось выяснить, что ключевые изменения внес коммит, который включает поддержку HTTP2 по умолчанию в стандартном транспорте.

Сразу возникает предположение, что проблема в работе HTTP2 на стороне сервера Twitter, точнее сервера, который обслуживает блог. Проверим с отключенной поддержкой HTTP2, для этого добавим к строке запуска переменную окружения `GODEBUG=http2client=0`:

docker run --rm -it -v "$PWD":/usr/srv/myapp -w /usr/srv/myapp "golang:${version}" sh -c "GODEBUG=http2client=0 go run main.go"

Запускаем:

1.4.3-alpine: Content-Encoding:
1.5.1-alpine: Content-Encoding:
1.5.2-alpine: Content-Encoding:
1.5.3-alpine: Content-Encoding:
1.5.4-alpine: Content-Encoding:
1.6.0-alpine: Content-Encoding:
1.6.1-alpine: Content-Encoding:
1.6.2-alpine: Content-Encoding:
1.6.3-alpine: Content-Encoding:
1.6.4-alpine: Content-Encoding:
1.7.0-alpine: Content-Encoding:
1.7.1-alpine: Content-Encoding:
1.7.3-alpine: Content-Encoding:
1.7.4-alpine: Content-Encoding:
1.7.5-alpine: Content-Encoding:
1.8.0-alpine: Content-Encoding:
1.8.1-alpine: Content-Encoding:

Curl, собранный с поддержкой протокола HTTP2, подтверждает наличие проблемы со стороны сервиса Twitter. Далее список проблемных ссылок и хостов, которые удалось найти за время всех экспериментов:

Остается только написать в Twitter и ждать исправления проблемы.

Дополнение от 3 мая

Оказалось, что все сервисы Твиттера, поддерживающие HTTP2, сжимают ответ вне зависимости от того, поддерживает клиент сжатие или нет. Где-то только gzip, где-то – deflate. На сервисах работающих по HTTP/1.1 проблем не замечено.

2017   go   golang   http2   twitter