Понадобилось мне однажды у себя в проекте реализовать работу с файловым хранилищем с использованием 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.1rnHost: system.restfs.test:9990rnContent-Length: 13864rncontent-type: audio/x-wavrnaccept-encoding: gzip, deflaternuser-agent: Python-httplib2/0.8 (gzip)rnrn'
send: <open file '/home/mixo/xd0x97xd0xb0xd0xb3xd1x80xd1x83xd0xb7xd0xbaxd0xb8/12.wav', mode 'r' at 0x7fca638c4db0>
sendIng a read()able
reply: 'HTTP/1.1 201 Createdrn'
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.1rnHost: system.restfs.test:9990rnContent-Length: 13864rncontent-type: audio/x-wavrnaccept-encoding: gzip, deflaternuser-agent: Python-httplib2/0.8 (gzip)rnrn'
send: <open file '/home/mixo/xd0x97xd0xb0xd0xb3xd1x80xd1x83xd0xb7xd0xbaxd0xb8/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.1rnHost: system.restfs.test:9990rnContent-Length: 13864rncontent-type: audio/x-wavrnaccept-encoding: gzip, deflaternuser-agent: Python-httplib2/0.8 (gzip)rnrn'
send: <open file '/home/mixo/xd0x97xd0xb0xd0xb3xd1x80xd1x83xd0xb7xd0xbaxd0xb8/12.wav', mode 'r' at 0x7f167a933030>
sendIng a read()able
reply: 'HTTP/1.1 500 Internal Server Errorrn'
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. На данный момент найденная проблема не имеет окончательного решения, но есть большое желание довести её до победного финала.
Надеюсь, этот пост будет полезен. Спасибо за внимание.
Автор: gmixo