Понадобилось мне однажды у себя в проекте реализовать работу с файловым хранилищем с использованием HTTP REST API. Проект разрабатывается на python, к тому же уже был реализован http-клиент с использованием библиотеки httplib2, поэтому было решено расширить функциональность http-клиента и работать с файловым хранилищем через туже библиотеку. Проблема возникла при загрузке файлов на сервер. Первый PUT запрос выполняется, далее все последующие запросы отказываются выполняться — 500 Internal Server Error.

Смотрю Wireshark'ом выясняется что после первого запроса сервер посылает в заголовках ответа connection: keep-alive и следом через 5 секунд закрывает соединение. Всё просто — это таймаут keep-alive установлен на сервере.



А вот как это выглядит на клиенте:

Включаю дебажные логи для httplib2:

httplib2.debuglevel = 4

Выполняю PUT запрос на клиенте:

res = rq.request(url, 'PUT', mediafile, h)
connect: (system.restfs.test, 9990) ************
send: 'PUT /domain/p.city/sounds/test_folder/12.wav HTTP/1.1\r\nHost: system.restfs.test:9990\r\nContent-Length: 13864\r\ncontent-type: audio/x-wav\r\naccept-encoding: gzip, deflate\r\nuser-agent: Python-httplib2/0.8 (gzip)\r\n\r\n'
send: <open file '/home/mixo/\xd0\x97\xd0\xb0\xd0\xb3\xd1\x80\xd1\x83\xd0\xb7\xd0\xba\xd0\xb8/12.wav', mode 'r' at 0x7fca638c4db0>
sendIng a read()able
reply: 'HTTP/1.1 201 Created\r\n'
header: connection: keep-alive
header: server: Cowboy
header: date: Fri, 11 Dec 2015 05:14:09 GMT
header: content-length: 0
header: content-type: audio/x-wav

Повторный PUT запрос:

res = rq.request(url, 'PUT', mediafile, h)
send: 'PUT /domain/p.city/sounds/test_folder/12.wav HTTP/1.1\r\nHost: system.restfs.test:9990\r\nContent-Length: 13864\r\ncontent-type: audio/x-wav\r\naccept-encoding: gzip, deflate\r\nuser-agent: Python-httplib2/0.8 (gzip)\r\n\r\n'
send: <open file '/home/mixo/\xd0\x97\xd0\xb0\xd0\xb3\xd1\x80\xd1\x83\xd0\xb7\xd0\xba\xd0\xb8/12.wav', mode 'r' at 0x7f167a933030>
sendIng a read()able
reply: ''
connect: (system.restfs.test, 9990) ************
send: 'PUT /domain/p.city/sounds/test_folder/12.wav HTTP/1.1\r\nHost: system.restfs.test:9990\r\nContent-Length: 13864\r\ncontent-type: audio/x-wav\r\naccept-encoding: gzip, deflate\r\nuser-agent: Python-httplib2/0.8 (gzip)\r\n\r\n'
send: <open file '/home/mixo/\xd0\x97\xd0\xb0\xd0\xb3\xd1\x80\xd1\x83\xd0\xb7\xd0\xba\xd0\xb8/12.wav', mode 'r' at 0x7f167a933030>
sendIng a read()able
reply: 'HTTP/1.1 500 Internal Server Error\r\n'
header: connection: keep-alive
header: server: Cowboy
header: date: Fri, 11 Dec 2015 05:26:27 GMT
header: content-length: 0
header: content-type: audio/x-wav

Здесь мы видим, что httplib2 честно, как и предписано сервером, не переустанавливает соединение и в тот же сокет отправляет новый запрос, не получив ответ, заново устанавливается соединение и посылает повторный запрос. Но этот повторный запрос уже не обрабатывается сервером, а возвращается ошибка 500.



При этом, если сравнить логи Wireshark для двух запросов, видно, что после переустановки соединения файл не отправляется и запрос обрывается и отправляется не полностью.

Тут же в качестве временного решения было выбрано выставлять в ответе на сервере заголовок connection: close. Этот вариант оказался рабочим: приятно почувствовать, что ты на верном пути, и решение близко.

Но не так близко, как я тогда думал. После изучения исходного кода httplib (которую расширяет httplib2) было выбрано более простое решение и создан pull-request для httplib:



В ходе детального рассмотрения pull-request'а совместно с ребятами из поддержки выяснилось что проблема находится на стыке библиотеки httplib и библиотеки httplib2:

  • httplib отправляет запрос и вычитывает файл в BODY;
  • httplib2 переустанавливает соединение с сервером и отправляет запрос повторно, но файл уже прочитан, и курсор находится в конце файла.

В случае повторной отправки запроса нужно начать читать файл сначала. Осталось выбрать виновного и казнить избрать меру пресечения. В библиотеке httplib при отправке файла предполагается, что если объект имеет метод read(), то его можно считать и передать. Можно ли также предположить, что этот же объект имеет методы tell() и seek() и сделать возврат курсора в начало файла. И если да, то эта логика все же должна быть вынесена в httplib2. В итоге был создан pull-request для httplib2. На данный момент найденная проблема не имеет окончательного решения, но есть большое желание довести её до победного финала.

Надеюсь, этот пост будет полезен. Спасибо за внимание.

Комментарии (0)