Преамбула: в один из дней мы решили подключить к нашему сайту CDN, для того, чтобы радовать пользователей более быстрой загрузкой страниц. После некоторых поисков выбор пал на Highwinds, т.к. они заявляли, что поддерживают весь нужный функционал и с ними удалось договориться на очень вкусную цену. После успешного перевода сайта на работу через Highwinds мы решили— а почему бы не переключить на них и наше REST API для мобильных приложений. И тут начались интересности.
Переключили API на тестовых девайсах на работу через CDN, проверяем: iOS работает, Android тоже вроде работает, хотя постойте. В Android приложении работают только GET и HEAD запросы, а POST, PUT и тд падают с 502. После недолгого разбирательства и сравнения трафика iOS и Android приложений выясняем, что Android отправляет заголовок «Transfer-Encoding:chunked» в запросах.
Пробуем дернуть страницу API curl'ом:
curl https://cdn.api.example.com -XPOST -d 'test=data'
Работает. А что если попробовать вот так:
curl https://cdn.api.example.com -XPOST -d 'test=data' -H 'Transfer-Encoding: chunked'
Ага, не работает, при том, что без использования CDN такие запросы отлично проходят.
В access логах нашего nginx видим, что запросы упали с кодом 400 «Bad request».
Но может быть проблема в том, что curl отправляет заголовок «Transfer-Encoding:chunked», но не формирует данные должным образом. Проверим этот вариант написав небольшой скрипт на Python, который отправляет данные чанками.
import requests
import logging
import httplib as http_client
http_client.HTTPConnection.debuglevel = 1
logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True
def test():
yield 'data'
yield 'test'
s = requests.Session()
data = s.post('https://cdn.api.example.com', data=test())
Скрипт висит 30 секунд (30 секунд это request write timeout в настройках CDN) и вываливается с ошибкой.
В выводе видно следующее:
send: 'POST cdn.api.example.com HTTP/1.1rnHost: cdn.api.example.comrnConnection: keep-alivernAccept-Encoding: gzip, deflaternAccept: */*rnUser-Agent: python-requests/2.18.4rnTransfer-Encoding: chunkedrnrn'
send: '4'
send: 'rn'
send: 'data'
send: 'rn'
send: '4'
send: 'rn'
send: 'test'
send: 'rn'
send: '0rnrn'
reply: 'HTTP/1.1 502 Bad Gatewayrn'
header: Date: Mon, 11 Dec 2017 22:05:04 GMT
header: Connection: Keep-Alive
header: Accept-Ranges: bytes
header: Cache-Control: max-age=10
header: Content-Length: 0
header: X-HW: 1
Видно, что запрос корректный, после последнего чанка идет сообщение «0rnrn» нулевой длины, сообщающее web-серверу, что все чанки переданы. Но сервер CDN продолжает ждать еще чанки и через 30 секунд отваливается по таймауту.
Но еще рано сваливать всю вину на CDN. Как мы помним до нашего nginx запрос доходит, но отваливается с кодом 400, возможно ли, что виноват наш nginx? Проверим это сделав дамп трафика и выбрав в Wireshark опцию «Follow TCP Stream», чтобы видеть данные в читабельном формате:
POST / HTTP/1.1
Date: Tue, 12 Dec 2017 07:19:48 GMT
Host: cdn.api.example.com
Connection: Keep-Alive
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: python-requests/2.18.4
Transfer-Encoding: chunked
Как видно nginx получил заголовки, но POST data до него ни в каком виде не дошла и когда сервер CDN отдает клиенту 502 и разрывает соединение с nginx ему не остается ничего, кроме как записать в лог сообщение о том, что он получил невалидный запрос.
Рассмотрим последнюю возможность, может быть CDN не обязан работать с «Transfer-Encoding:chunked» и мы сами виноваты, что использовали его в приложении? Почитаем, что про это думает RFC 2616. То, что мы ищем нашлось в секции 3.6.1. По стандарту использование «Transfer-Encoding:chunked» разрешено как в запросах, так и в ответах. Отдельно указывается, что это обязательная часть HTTP/1.1 и она должна поддерживаться во всех приложениях, реализующих данный стандарт.
Мы собрали на руках карт-бланш, доказывающий, что проблема в неправильной работе HTTP сервера на стороне CDN. Пишем тикет в саппорт и после долгого выяснения всех деталей проблемы и общения с их инженерами получаем замечательный ответ.
We have confirmed that this is not a bug in our system and that chunked encoding in request is not working by design.
После такого даже и добавить особо нечего. Отдельно хочу заметить, что проблема не возникла, если бы Highwinds использовали любую Open Source реализацию HTTP сервера, например Varnish или Nginx, а не писали свою с такими фичами «by design». Остерегайтесь подделок HTTP протокола.
Автор: Владимир