Если поискать в интернете решение задачи «How to read HttpServletRequest multiple times», то можно найти множество ответов - и на Stack Overflow, и на Baeldung. Но все ли они подходят для всех случаев?
В чем вообще проблема.
Оказывается, что используемые в Spring Boot по умолчанию реализации HttpServletRequest
предоставляют возможность прочитать InputStream
, который содержит байты тела запроса, только один раз. Все следующие вызовы getInputStream()
возвращают null или выбрасывают исключение.
А зачем вообще может понадобиться несколько раз читать тело запроса? Возможно вам захочется валидировать входные параметры в самописном jakarta.servlet.Filter
. Кто-то реализует аутентификацию в теле запроса. В моей команде используется самописная библиотека, которая логирует тело запроса и ответа в целях отладки. В общем никогда не знаешь, что и зачем может понадобиться.
Первая реализация библиотеки для логирования запросов и ответов использовала multiple-read-servlet от Jamie Tanna. К сожалению, автор перестал поддерживать свое творение, и при переезде на новые версии Spring Boot и Java пришлось написать свою реализацию MultiReadHttpServletRequest
, хоть она почти и не отличалась от библиотечной (за исключением некоторых импортов, переехавших из javax в jakarta). На этом мы успокоились и использовали библиотеку еще некоторое время.
Проблема с application/x-www-form-urlencoded
Пока мы работали только с телом запроса и ответа в формате JSON, всё шло хорошо. Но в какой-то момент понадобилось работать с запросами, которые содержат данные в формате application/x-www-form-urlencoded
. С удивлением я обнаружил, что до Spring контроллера запросы приходят уже без тела.
Покопавшись внутри обработчиков запросов, я нашёл, что Tomcat Request имеет такое поведение - при запросе multipart параметров он смотрит, читали ли уже данные из тела запроса, и если читали, то ничего не возвращает. И получалось так, что первый раз тело запроса было прочитано (и косвенно поднять флаг usingInputStream) в классе MultiReadHttpServletRequest
самописной библиотеки, при получении InputStream для кеширования.
На этом этапе начало вырисовываться решение - нужно кешировать не только InputStream тела, но и параметры запроса. К счастью, решение удалось подсмотреть в самом Spring - похожий механизм есть в ContentCachingRequestWrapper, в нём переопределяются методы возвращения параметров. При вызове методов кешируется содержимое параметров. Таким образом была решена проблема с запросами x-www-form-urlencoded
.
Проблема с multipart/form-data
Дальше появились трудности, когда мы решили принимать файлы (запросы формата multipart/form-data
). Начало было аналогичным - до контроллера запрос доходит уже без тела, а при отключении самописной библиотеки для логирования тело запроса появлялось.
Решение проблемы, по аналогии с предыдущей, тоже понятно - нужно кешировать все Parts
нашего многосоставного запроса. Но как кешировать? Оказалось, что интернет нам не помощник, всё, что удалось найти, сводилось к Deprecated-файлу, который говорил нам - используй библиотеку, которой нет в open source. Если не смогли скопипастить, напишем сами. В итоге получилось такое решение - при создании MultiReadHttpServletRequest
из оригинального Request
вычитываем и кешируем все Parts
, а при всех следующих обращениях за этим Parts
- отдаем из кеша.
Итоги
В результате получился класс, который кеширует как запросы с телом, так и запросы с ContentType application/x-www-form-urlencoded
и multipart/form-data
. Это позволяет получать отладочную информацию в логах и при этом не ломает остальную работу приложений. Код класса приведён на GitHub.