RFC для слабаков или история одного расследования

    Преамбула: в один из дней мы решили подключить к нашему сайту 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.1\r\nHost: cdn.api.example.com\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: python-requests/2.18.4\r\nTransfer-Encoding: chunked\r\n\r\n'
    send: '4'
    send: '\r\n'
    send: 'data'
    send: '\r\n'
    send: '4'
    send: '\r\n'
    send: 'test'
    send: '\r\n'
    send: '0\r\n\r\n'
    reply: 'HTTP/1.1 502 Bad Gateway\r\n'
    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
    

    Видно, что запрос корректный, после последнего чанка идет сообщение «0\r\n\r\n» нулевой длины, сообщающее 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 7230. То, что мы ищем нашлось в секциях 3.3.1 и 4.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 протокола.
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 21
    • –9
      «Мы собрали на руках карт-бланш» — когда хотелось выпендриться, но получилось только опозориться.
      • +6
        Материал интересный, зачем так язвить?
      • 0

        В идеале бы не ссылаться на устаревший RFC (в новом, насколько я помню, плюс-минус тоже самое, но).

        • 0
          Спасибо, заменил ссылку на RFC 7230.
        • +4
          Материал действительно интересный, но у «кард-бланш» значение несколько другое, проверьте ;).
          • +2
            Спасибо, заменил на более подходящее по смыслу выражение.
          • 0
            Вот поэтому и дёшево. Надеюсь вы сменили провайдера CDN?
            • 0
              Пока еще думаем как с этим жить дальше и к кому можно уйти.
              • +1
                Дешевле разрабатывать собственный кривой сервер, чем взять открытый и бесплатный??
              • +1

                Поддержу Highwinds в данном споре. Chunked encoding совсем не для того, как вы его используете — он для случаев, когда на стороне источника потока данных есть какая-то относительно сложная логика, данных много и заранее неизвестно, когда они закончатся. Тело запроса к cdn — определённо не тот случай.

                • +3
                  он для случаев, когда на стороне источника потока данных есть какая-то относительно сложная логика, данных много и заранее неизвестно, когда они закончатся.

                  Да, и это не обязательно должен быть сервер, это может быть и клиент. И в спецификации HTTP сказано, что этот функционал должен быть реализован.
                  • +3

                    Да, может быть и клиент. Но (может я конечно не так понял?) в вашем случае нет множества данных которые неизвестно когда закончатся, а есть короткий запрос.
                    Зачем вот слать вот такое:


                    Transfer-Encoding: chunked\r\n\r\n
                    4\r\ndata\r\n
                    4\r\ntest\r\n
                    0\r\n\r\n

                    Когда можно послать так:


                    Content-Length: 8\r\n\r\n
                    datatest

                    .


                    Отдельно хочу заметить, что проблема не возникла, если бы Highwinds использовали любую Open Source реализацию HTTP сервера, например Varnish или Nginx, а не писали свою

                    Как раз написали свою, оптимизированную под работу в режиме CDN (а не http-сервера общего назначения, где может быть обоснованно нужен первый вариант), для сокращения расходов на эксплуатацию, и смогли предложить вам дешевые услуги. По-моему это с их стороны правильное действие.

                    • +1
                      Ну это всего лишь тестовый скрипт. В Android приложении, как я понимаю дело было в том, что программисты отправляли в http клиент данные через stream buffer, а он отправлял данные чанками по мере получения.
                    • 0
                      А вы рассматривайте CDN как не поддерживающую POST-запросы вообще. Спецификация HTTP это не запрещает :-)
                      • 0
                        К сожалению не получится. Когда подключали cdn проводили тестирование скорости и получился следующий топ (от самого быстрого к медленному, замеряли время dom ready на клиентах):
                        1) Статика на основном домене через CDN
                        2) Статика на основном домене без CDN
                        3) Статика на отдельном домене через CDN
                        4) Статика на отдельном домене без CDN
                        То есть использовать CDN есть смысл только в 1 случае, когда через cdn проксируется весь трафик сайта, а статика лежит на том же домене. Работать в таком режиме без POST запросов естественно нельзя. Вообще тема достаточно большая и если интересно могу написать отдельную статью о том, как мы это замеряли и к каким выводам пришли. ;)
                        • +1

                          Очень странно, что у вас статика на отдельном домене медленнее.
                          Я бы вам рекомендовал найти причину этого, т.к. обычно ситуация ровно обратная.
                          Может у вас для разных доменов разные ssl сертификаты (ну и 2 https соединения установить дольше, чем одно)?


                          Вообще, заводя логику через CDN, вы расставляете очень много грабель.
                          А если те ребята так обращаются, с RFC…
                          Вы бы узнали, например, как они обрабатывают другие заголовки, например Cache-Control.

                          • 0
                            Да, у нас есть своя специфика. Во первых сайт довольно легкий, страница весит около 500 кб, то есть она грузится достаточно быстро и дополнительный tcp+ssl хендшейк на отдельный домен тратит больше времени, чем выигрывает. Особенно если клиент в US или Австралии (сервера у нас в Европе). А если соединение по HTTP/2, то разница еще заметнее.
                        • +4

                          В этом случае CDN на POST должен отвечать 405 Method Not Allowed :-)

                    • +3
                      если бы Highwinds использовали любую Open Source реализацию HTTP сервера, например Varnish или Nginx

                      Справедливости ради, nginx очень долго — до версии 1.3.9 — не поддерживал chunked encoding в запросах. Может, у них форк старого nginx.

                      • 0
                        Один штрих для понимания картины: вы нашли проблему в фазе тестирования сервиса или в продакшене?
                        • 0
                          Нашли уже после того, как выкатили сайт через CDN (и он пока продолжает так работать, т.к. данная проблема не затрагивает браузеры), но до включения CDN для REST API (оно работает на отдельном домене).

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