Особенности работы с русской кодировкой при загрузке файлов через aiohttp

В ходе исследования непонятного бага с битой кодировкой в именах загружаемых файлов мы столкнулись с непредвиденным поведением популярной библиотеки aiohttp. Решая эту проблему, мы получили полезный опыт, которым хочу с вами поделиться.

Описание ошибки

48483c93a4b0400ae723b44ba42ca710.png

Наткнулись на ошибку случайно: в одной из интеграций стояла «маска» на символы в имени файлов, и в какой-то момент сработало оповещение о количестве ошибок в отправленных запросах. Исходные файлы имели имена вида «тест.png», но после загрузки в систему превращались в »%D1%82%D0%B5%D1%81%D1%82.png».

Исправление ошибки

3c9b97b255cdc7ff9aa2327014f84c27.png

Для решения проблемы пришлось последовательно проверить каждую точку взаимодействия с этим файлом в системе, и в итоге нашли место с превращением файла «тест.pdf» в »%D1%82%D0…». 

Для загрузке на Filestorage мы использовали представленный в базовой документации aiohttp способ загрузки (файлы небольшого размера):

url = 'http://httpbin.org/post'
files = {'file': open('тест.png', 'rb')}
await session.post(url, data=files)

Переход на способ загрузки с FormData() не решал проблему:

url = 'http://httpbin.org/post'
data = FormData()
data.add_field('file',
               open('тест.png', 'rb'),
               filename='тест.png',
               content_type='image/png')

await session.post(url, data=data)

Быстро разобраться в таком поведении библиотеки aiohttp не получилось, поэтому пришлось глубже изучить документацию, провалиться в реализацию post-метода самой библиотеки и изучить похожие вопросы на просторах интернета. Как ни странно, на такую проблему наткнулись коллеги при использовании китайских символов, и способ решения был рабочий:

url = 'http://httpbin.org/post'
# По умолчанию quote_fields=True, что приводит к замещению русских букв на %xx
data = FormData(quote_fields=False)  
data.add_field('file',
               open('тест.png', 'rb'),
               filename='тест.png',
               content_type='image/png')

await session.post(url, data=data)

Осталось понять почему библиотека так себя ведёт. Оказалось, всё дело в стандарте RFC 7578: по умолчанию выполняется квотирование значений для 7-битных MIME-заголовков. Если окружающие сервисы поддерживают имена файлов с расширенными ASCII-символами (больше 7 бит), то смело указываем 
quote_fields=False, и тогда проблема с нелатинскими буквами уйдёт.

Заключение

0ce2efad3fa4ed4f1ad5dae87cad783d.png

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

© Habrahabr.ru