Pull to refresh

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

Reading time 3 min
Views 19K
Преамбула: в один из дней мы решили подключить к нашему сайту 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 протокола.
Tags:
Hubs:
+60
Comments 23
Comments Comments 23

Articles