Отладка микросервисных автотестов: от сетевых путей до логики Spring

Курс предназначен для быстрого погружения в диагностику ошибок 404 в распределенных системах. Вы научитесь прослеживать путь запроса через Ingress и Context Path, анализировать Swagger-контракты и находить причины расхождений между документацией и реальностью.

1. Анатомия запроса: как Context Path и Ingress определяют маршрут в Kubernetes

Анатомия запроса: как Context Path и Ingress определяют маршрут в Kubernetes

Вы добавили префикс /aeqs-backend-preregistration к URL, и тест для /time_slots внезапно позеленел, вернув заветный статус 200 OK. Но стоило автотесту перейти к следующему шагу и отправить запрос на /reservation с тем же самым префиксом — система снова выбросила 404 Not Found. Почему одна и та же дверь открывается для первого запроса, но наглухо заперта для второго?

Чтобы ответить на этот вопрос и сузить зону поиска ошибки в задаче AEQS-49, нам нужно понять, как именно ваш HTTP-запрос путешествует от скрипта автотеста до конкретного метода в Java-коде.

Два уровня маршрутизации

Когда автотест выполняет команду curl, запрос не попадает в микросервис мгновенно. Он проходит через два независимых контроля пропуска: внешний (сетевой) и внутренний (аппликационный). Ошибка 404 может возникнуть на любом из них, и лечатся они совершенно по-разному.

!Схема маршрутизации запроса в Kubernetes

Уровень 1: Ingress — диспетчер кластера

В современной микросервисной архитектуре приложения живут внутри изолированных контейнеров (подов) в Kubernetes. Снаружи они недоступны. Чтобы ваш автотест мог «достучаться» до пода aeqs-backend-preregistration, на границе кластера стоит Ingress-контроллер.

Ingress работает как диспетчер в большом офисном здании. Он смотрит на доменное имя в вашем запросе (aeqs-backend-preregistration.dev.aeqs.corp.dev.vtb) и принимает решение, в какой именно под направить трафик.

> Ingress — это правило маршрутизации Kubernetes, которое управляет внешним доступом к сервисам в кластере, обычно на основе HTTP/HTTPS хостов и путей.

Если Ingress не знает указанного домена или базового пути, он сам вернет 404 Not Found. В этом случае запрос даже не дойдет до Java-приложения. В логах вашего пода будет абсолютно пусто.

Уровень 2: Context Path — коридор внутри приложения

Допустим, Ingress пропустил запрос и доставил его прямо в под aeqs-backend-preregistration. Здесь в игру вступает сам фреймворк Spring Boot.

По умолчанию Spring Boot ожидает, что пути запросов начинаются от корня (/). То есть контроллер ждет путь /v1/api/time_slots. Однако в конфигурации вашего пода (как вы обнаружили в логах) жестко задана переменная окружения: CONTEXT_PATH=/aeqs-backend-preregistration

Context Path — это глобальный префикс для всего приложения. Если он задан, Spring Boot «переезжает» в этот виртуальный коридор. Теперь приложение игнорирует любые запросы, которые не начинаются с этого префикса.

Вот почему ваша Фаза 1 увенчалась успехом:

  • Автотест отправлял: GET /v1/api/time_slots
  • Spring Boot видел это и говорил: «Я обслуживаю только то, что начинается с /aeqs-backend-preregistration. Такого пути я не знаю. Держи 404».
  • Вы изменили URL на /aeqs-backend-preregistration/v1/api/time_slots.
  • Spring Boot отрезал свой Context Path, увидел знакомый /v1/api/time_slots, нашел нужный метод и вернул 200 OK.
  • Разбор парадокса AEQS-49

    Теперь применим эти знания к вашей текущей проблеме с /reservation. У нас есть неоспоримый факт: метод /time_slots с новым URL работает корректно.

    Что этот факт говорит нам о состоянии системы?

  • Ingress настроен правильно: трафик успешно достигает пода. Сетевых блокировок нет.
  • Под жив: контейнер Running, приложение обрабатывает запросы.
  • Context Path определен верно: префикс /aeqs-backend-preregistration — это именно то, что ожидает Spring Boot.
  • Из этого следует важнейший вывод для отладки: ошибка 404 при вызове /reservation генерируется не инфраструктурой (Kubernetes/Ingress), а самим приложением Spring Boot.

    Ваш запрос успешно проходит диспетчера, заходит в нужный коридор Context Path, стучится в дверь контроллера, но Spring Boot по какой-то причине не может сопоставить этот конкретный запрос с Java-методом, отвечающим за бронирование.

    Проверка гипотез

    В вашем описании проблемы были выдвинуты отличные гипотезы. Давайте проверим их через призму того, что мы только что узнали:

    | Гипотеза | Вердикт на основе архитектуры | | :--- | :--- | | Есть ли скрытая версия/префикс для бронирования? | Крайне маловероятно. Context Path применяется ко всему приложению целиком. Если бы префикс был другим, /time_slots тоже бы перестал работать. | | Метод переехал в другой сервис (например, aeqs-ticket)? | Вполне возможно. Если логику вынесли в другой микросервис, Ingress всё равно доставит запрос в старый под (так как домен старый), но Spring Boot уже не найдет у себя этого метода и честно вернет 404. | | Влияет ли отсутствие параметра userSession на маршрутизацию? | Да, это частая причина. В Spring Boot можно настроить маршрутизацию так, что метод контроллера активируется только если в запросе присутствует определенный параметр или заголовок. Если параметра нет — для Spring Boot этого маршрута не существует (снова 404). |

    Мы локализовали проблему. Сеть и базовые настройки путей в порядке. Запрос находится внутри контейнера. В следующем шаге мы заглянем внутрь самого Spring Boot, чтобы понять, как он принимает решение отклонить запрос из-за отсутствующих параметров или несовпадения контрактов.

    2. Диагностика 404: разграничение сетевых проблем и ошибок внутри Spring Boot приложения

    Диагностика 404: разграничение сетевых проблем и ошибок внутри Spring Boot приложения

    В одном и том же микросервисе метод GET /v1/api/time_slots радостно отвечает статусом 200, а соседний POST /v1/api/reservation упрямо выдает 404 Not Found. Трафик идет через один Ingress, стучится в один и тот же под, использует один и тот же CONTEXT_PATH. Как сервер может «не видеть» половину самого себя?

    Ошибка 404 (Not Found) в микросервисной архитектуре коварна тем, что у нее есть два совершенно разных источника. Чтобы не тратить часы на поиск проблемы не в том месте, нужно четко провести границу между инфраструктурой и логикой приложения.

    Два лица ошибки 404

    Когда автотест отправляет запрос, он может получить 404 на двух разных этапах пути:

    | Этап | Источник 404 | Причина | Симптомы | | :--- | :--- | :--- | :--- | | Внешний (Сеть) | Ingress-контроллер или API Gateway | Запрос пришел на кластер, но сетевой маршрутизатор не знает, в какой под его направить. | Ни один метод сервиса не работает. В логах пода нет никаких следов запроса. | | Внутренний (Приложение) | Spring Boot | Запрос успешно доставлен в под, но само Java-приложение отказалось его обрабатывать, так как не нашло подходящего метода. | Одни методы работают, другие — нет. В логах пода (при включенном уровне DEBUG) видно, что запрос пришел. |

    В задаче AEQS-49 мы уже знаем, что /time_slots работает. Это неопровержимое доказательство того, что внешний сетевой контур настроен идеально. Значит, наш 404 генерируется внутри самого пода aeqs-backend-preregistration. Приложение получает запрос на бронирование, смотрит на него и заявляет: «Я не знаю, что с этим делать».

    Как Spring Boot принимает решения: DispatcherServlet

    Чтобы понять, почему приложение отвергает валидный на первый взгляд URL, нужно посмотреть, как устроена маршрутизация внутри Spring Boot.

    > DispatcherServlet — это главный регулировщик трафика внутри веб-приложения Spring. Он принимает все входящие HTTP-запросы (уже очищенные от CONTEXT_PATH) и ищет для них подходящий Java-метод (контроллер), используя механизм Handler Mapping.

    Многие ошибочно полагают, что DispatcherServlet ищет совпадение только по строке URL. Если бы это было так, наш /v1/api/reservation отработал бы успешно, ведь он заявлен в Swagger. На самом деле Spring оценивает запрос по целой матрице условий.

    Метод в Java-коде может выглядеть так:

    Чтобы DispatcherServlet пустил запрос к этому коду, должно совпасть всё:

  • URI: Точное совпадение пути (/v1/api/reservation).
  • HTTP-метод: Если контроллер ждет POST, а тест шлет GET или PUT, Spring вернет 405 (Method Not Allowed) или 404.
  • Content-Type (consumes): Если контроллер ждет application/json, а в заголовках теста указано что-то другое (или заголовок опущен), маршрут не совпадет.
  • Заголовки (headers) и параметры (params): Контроллер может быть жестко настроен на наличие определенного заголовка или query-параметра прямо на уровне маршрутизации.
  • Иллюзия «отсутствующего» метода

    Здесь кроется ответ на вопрос, почему отсутствие параметра userSession может приводить именно к 404 ошибке.

    Обычно, если клиент забыл передать обязательное поле, сервер возвращает 400 Bad Request (неверный запрос). Это происходит, если параметр валидируется внутри метода. Но разработчики могут использовать параметры для строгой маршрутизации.

    Если в коде бэкенда написано:

    Это означает: «Считай этот метод существующим только если в URL есть параметр userSession». Если параметра нет, DispatcherServlet даже не попытается выполнить код. Для него этого маршрута в данный момент просто не существует. Отсюда — закономерный 404 Not Found.

    Тот факт, что /time_slots успешно отрабатывает без userSession, говорит лишь о том, что для получения слотов разработчики не заложили таких строгих правил маршрутизации. А вот создание бронирования (/reservation), являясь операцией изменения данных, может иметь более жесткий контракт.

    Чтобы подтвердить или опровергнуть эту гипотезу, нам больше не нужно смотреть на сеть. Нам нужно заглянуть в контракты сервиса и сопоставить то, что ожидает Spring, с тем, что реально отправляет автотест.

    3. Чтение Swagger как карты: сопоставление эндпоинтов, параметров и реальных контроллеров

    Чтение Swagger как карты: сопоставление эндпоинтов, параметров и реальных контроллеров

    В Swagger-документации сервиса aeqs-backend-preregistration описаны два соседних метода: /time_slots и /reservation. У них идентичная структура путей. Для обоих указан обязательный query-параметр userSession. В автотесте этот параметр не передаётся ни в одном из вызовов. Парадокс: запрос за слотами успешно возвращает статус 200, а попытка создать бронирование разбивается о 404 Not Found. Как два метода, описанные в одном контракте по одним и тем же правилам, могут вести себя настолько по-разному?

    В прошлой главе мы выяснили, что ошибка 404 генерируется внутри Spring Boot компонентом DispatcherServlet, когда он не может подобрать Handler Mapping для входящего запроса. Теперь пришло время разобраться, почему Swagger показывает, что маршрут существует, а Spring утверждает обратное.

    Иллюзия контракта: Swagger — это не код

    Главная ошибка при отладке микросервисов — воспринимать Swagger UI как точное отражение исполняемого кода.

    Swagger (или, точнее, OpenAPI Specification) — это лишь декларация намерений. В экосистеме Spring Boot эта документация генерируется библиотеками (например, springdoc-openapi) на основе аннотаций в коде.

    > Swagger описывает, как клиент должен общаться с API, но он не управляет тем, как Spring фактически маршрутизирует этот запрос.

    Разрыв между документацией и реальностью возникает постоянно. Разработчик мог обновить логику валидации, но забыть обновить аннотации @Operation или @Parameter. Или, что более критично для нашей задачи AEQS-49, метод мог быть описан на уровне Java-интерфейса, который сканируется Swagger'ом, но его реальная реализация в контроллере была изменена или удалена.

    Анатомия параметра userSession

    В Swagger параметр userSession описан как query-параметр, но при этом представляет собой сложный объект с полями id, created, phone и другими.

    В HTTP не существует стандарта передачи JSON-объектов прямо в строке запроса (query string). Spring Boot по умолчанию пытается собрать такой объект, сопоставляя плоские параметры из URL с полями Java-класса. То есть фреймворк ожидает увидеть в URL что-то вроде ?id=123&phone=79990001122.

    Как отсутствие этих параметров влияет на маршрутизацию? Зависит от того, как написан код контроллера.

    | Реализация в Spring Boot | Поведение при отсутствии параметра | Результат для клиента | | :--- | :--- | :--- | | @RequestParam(required = true) String id | DispatcherServlet находит маршрут, но валидатор параметров прерывает выполнение. | 400 Bad Request (Отсутствует обязательный параметр) | | @RequestMapping(path = "/reservation", params = "userSession") | DispatcherServlet исключает этот метод из матрицы Handler Mapping, так как условие маршрута не выполнено. | 404 Not Found (Подходящий обработчик не найден) | | UserSession session (без строгих аннотаций) | Spring создает пустой объект (все поля null) и передает его в метод. | 200 OK или 500 (если внутри кода возникнет NullPointerException) |

    !Влияние аннотаций на статус ответа

    Успех /time_slots без userSession говорит о том, что для этого метода реализован третий сценарий: параметр задекларирован в Swagger, но в коде контроллера он не является строго обязательным для маршрутизации или валидации.

    А вот 404 на /reservation указывает на одно из двух: либо здесь применен второй сценарий (жесткая привязка маршрута к наличию параметра), либо метод физически отсутствует в этом приложении.

    Гипотеза «Призрачного эндпоинта»

    Вы выдвинули логичное предположение: «Может ли логика бронирования быть перенесена в другой сервис (например, aeqs-ticket), при этом Swagger-схема осталась старой?»

    В микросервисной архитектуре это классический паттерн расхождения. Процесс миграции часто выглядит так:

  • Команда создает новый сервис aeqs-ticket и переносит туда логику /reservation.
  • В старом сервисе aeqs-backend-preregistration удаляют класс ReservationController.
  • Однако старые DTO (Data Transfer Objects) или интерфейсы API остаются в общих библиотеках (shared libraries), которые всё ещё подключены к старому сервису.
  • Генератор Swagger сканирует эти остаточные файлы и продолжает добросовестно отрисовывать /reservation в UI.
  • В результате Ingress корректно пропускает трафик в под aeqs-backend-preregistration (ведь Context Path верный), но DispatcherServlet внутри пода разводит руками — контроллера больше нет. И мы получаем честные 404 Not Found.

    !Как проверить наличие контроллера в поде

    Чтобы подтвердить или опровергнуть гипотезу о переезде метода, нам недостаточно смотреть в Swagger. Нам нужно заглянуть в реальную карту маршрутов Spring Boot, которая формируется в момент старта приложения. О том, как запросить эту информацию и проверить влияние валидации на маршрутизацию, мы поговорим на следующем этапе отладки.

    4. Загадка отсутствующих параметров: влияние Query-валидации на маршрутизацию запроса

    Заголовок статьи

    Вы отправляете POST-запрос на создание бронирования, забываете передать обязательный параметр userSession, и сервер отвечает ошибкой 404 Not Found. Здравый смысл подсказывает, что приложение должно было вернуть 400 Bad Request — ведь эндпоинт существует, просто вы прислали неполные данные. Почему же Spring Boot делает вид, что такого адреса вообще нет?

    Чтобы разгадать эту загадку в рамках задачи AEQS-49, нам нужно заглянуть под капот Spring и понять, в какой именно момент параметр запроса перестает быть просто «данными» и становится частью самого маршрута.

    Два лица параметров: Валидация против Маршрутизации

    В Spring Boot параметры запроса (Query Parameters) могут выполнять две принципиально разные роли. От того, какую роль выбрал разработчик для userSession, зависит HTTP-статус, который вы получите при ошибке.

    Сценарий 1: Параметр как часть полезной нагрузки (Валидация)

    Чаще всего параметры обрабатываются уже после того, как DispatcherServlet нашел нужный метод контроллера. Разработчик использует аннотацию @RequestParam:

    В этом случае маршрут привязан только к URL (/v1/api/reservation) и HTTP-методу (POST). Если автотест не передаст userSession, Spring успешно найдет этот метод, попытается передать в него данные, обнаружит нехватку обязательного аргумента и выбросит исключение MissingServletRequestParameterException. Для клиента это превратится в классический 400 Bad Request.

    Сценарий 2: Параметр как условие поиска (Строгая маршрутизация)

    Но существует и другой подход. Разработчик может указать наличие параметра как жесткое условие для самого Handler Mapping на уровне маппинга:

    Здесь атрибут params = "userSession" меняет правила игры. DispatcherServlet читает это так: «Я передам запрос в этот метод, только если URL совпадает, метод POST, и в строке запроса присутствует ключ userSession».

    Если автотест отправляет запрос без этого параметра, DispatcherServlet проверяет свои списки, видит, что условия не выполнены, и решает, что подходящего обработчика не существует. Результат — 404 Not Found.

    !Какую ошибку вернет Spring

    Сложные объекты в Query: ловушка Swagger

    В нашей задаче ситуация усложняется тем, что Swagger описывает userSession не как простую строку, а как объект с полями id, created, phone и другими.

    Передача сложных объектов через Query-параметры в GET или POST запросах — это всегда зона риска. Spring Boot умеет автоматически собирать такие объекты из плоского списка параметров (?id=123&phone=555), но в Swagger это часто отображается криво.

    Если в коде бэкенда контроллер ожидает строгую привязку к параметрам (например, params = "userSession.id"), а автотест их не передает, мы снова попадаем в ловушку строгой маршрутизации и получаем 404. Swagger в данном случае лишь показывает желаемую структуру, но не объясняет механику маппинга.

    Spring Boot Actuator: рентген маршрутизации

    Мы знаем, что Swagger может отставать от реальности. Чтобы перестать гадать, является ли отсутствие userSession причиной 404 ошибки, или же метод /reservation действительно переехал в другой микросервис (например, в aeqs-ticket), нам нужен объективный источник истины.

    Таким источником выступает модуль Spring Boot Actuator.

    > Spring Boot Actuator — это набор встроенных инструментов для мониторинга и управления приложением. Он позволяет увидеть приложение изнутри в момент его работы, минуя устаревшую документацию.

    Самый ценный для нас эндпоинт — /actuator/mappings. Если в DEV-контуре Actuator открыт, мы можем сделать к нему GET-запрос и получить полную карту всех маршрутов, которые DispatcherServlet знает прямо сейчас.

    Ответ Actuator выглядит как массив JSON-объектов, где каждый элемент описывает конкретный метод:

    Посмотрев в блок requestMappingConditions, мы сразу увидим массив params. Если он не пуст, значит, параметр является строгим условием маршрутизации. Если метод /reservation вообще отсутствует в выдаче Actuator, наша гипотеза о переносе логики в другой микросервис подтверждается, и Swagger действительно врет.

    !Зачем нужен Actuator

    Вместо того чтобы вслепую перебирать параметры в автотесте, мы получили четкий алгоритм: проверить реальные условия маппинга. На следующем этапе мы сопоставим то, что требует живой код, с тем, что описано в контрактах, чтобы окончательно локализовать проблему AEQS-49.

    5. Синхронизация контрактов: выявление расхождений между кодом сервиса и Swagger-схемой

    Синхронизация контрактов: выявление расхождений между кодом сервиса и Swagger-схемой

    В Swagger-документации сервиса aeqs-backend-preregistration метод POST /reservation описан чётко: указаны структура тела запроса и обязательный query-параметр userSession. Автотест отправляет запрос строго по этому URL, но получает в ответ HTTP 404 Not Found. Парадокс заключается в том, что интерфейс, призванный быть единым источником истины для тестировщика, в микросервисной архитектуре часто начинает жить собственной жизнью, оторванной от реального поведения скомпилированного кода.

    Дрейф контрактов: почему документация лжёт

    Swagger (OpenAPI) — это декларация намерений, а не гарантия исполнения. В Spring Boot проектах документация может генерироваться автоматически на основе аннотаций в коде, но даже в этом случае возникают рассинхронизации.

    > Дрейф контрактов (Contract Drift) — ситуация, при которой задекларированный API (например, в Swagger) перестаёт соответствовать реальным маршрутам, обрабатываемым приложением в данный момент времени.

    Разрыв контрактов можно выразить через множества. Если — множество эндпоинтов, описанных в документации, а — множество реальных маршрутов, зарегистрированных в памяти Spring, то в стабильной системе . Однако при активной разработке часто возникает ситуация .

    Основные причины дрейфа в контексте проблемы AEQS-49:

  • Отрыв аннотаций от реализации. Разработчик мог вынести логику бронирования в новый микросервис (например, aeqs-ticket), удалив сам метод контроллера в aeqs-backend-preregistration, но забыв удалить интерфейс с аннотациями @Operation и @ApiResponses, по которым строится Swagger.
  • Кэширование или статический файл. Swagger UI может отображать статический файл openapi.yaml, который не обновлялся на DEV-контуре после последних деплоев.
  • Скрытая строгая маршрутизация. Метод физически существует, но аннотация @RequestMapping требует специфичных условий (заголовков или параметров), которые не отражены в Swagger корректно, или отражены, но автотест их игнорирует.
  • !Причины несовпадения Swagger и реальности

    Сверка показаний: Swagger против Actuator

    Чтобы доказать или опровергнуть гипотезы по AEQS-49, необходимо столкнуть декларацию (Swagger) с реальностью (Actuator). Мы уже знаем, что /actuator/mappings выдаёт сырую карту всех активных маршрутов пода.

    Сценарий 1: Эндпоинт существует, проблема в параметрах

    Мы делаем GET-запрос к https://.../aeqs-backend-preregistration/actuator/mappings и ищем в JSON-ответе строку /v1/api/reservation.

    Если мы находим блок, похожий на этот:

    Это подтверждает гипотезу строгой маршрутизации. Метод живёт в этом сервисе, но Spring Boot требует наличия userSession прямо на этапе Handler Mapping. Без этого параметра запрос не дойдёт до контроллера, и DispatcherServlet вернёт 404. В этом случае Swagger не врёт, а автотест просто не выполняет контракт.

    Сценарий 2: Призрак миграции

    Мы скачиваем JSON от /actuator/mappings и выполняем поиск по слову reservation. Поиск даёт ноль результатов.

    Это неопровержимое доказательство того, что маршрут POST /v1/api/reservation физически не обслуживается подом aeqs-backend-preregistration. Swagger показывает «призрачный эндпоинт». Отсутствие параметра userSession в автотесте здесь ни при чём: даже если передать идеальный payload и все параметры, Spring Boot всё равно вернёт 404, потому что контроллера больше нет.

    Формирование аргументированного запроса к бэкенд-команде

    Отладка автотеста — это не только поиск ошибки в коде теста, но и локализация проблем на стороне сервиса. Сообщение разработчикам «тест падает с 404 на бронировании» приведёт к долгому пинг-понгу. Собранные факты позволяют сформулировать точный запрос.

    Вместо эмоций мы оперируем метриками: * Сетевая доступность подтверждена: соседний метод /time_slots отдаёт 200 OK. * Контракт в Swagger требует userSession, но тест падает с 404, а не с 400 Bad Request.

    Если проверка через Actuator показала Сценарий 2 (эндпоинта нет в маппингах), запрос к команде должен выглядеть так:

    > «Коллеги, в рамках AEQS-49 падает создание бронирования. > Swagger в aeqs-backend-preregistration показывает наличие POST /v1/api/reservation, однако выгрузка из /actuator/mappings этого пода подтверждает отсутствие данного маршрута в Handler Mapping. > Прошу подтвердить: была ли логика бронирования перенесена в другой сервис (например, aeqs-ticket)? Если да, предоставьте актуальный базовый URL для бронирования. Если нет — в поде отсутствует контроллер».

    !Формирование запроса разработчикам

    Такой подход переводит тестировщика из позиции «у меня что-то не работает» в позицию инженера, который провёл первичный аудит системы, сэкономив время всей команде.

    6. Анализ логов пода: поиск следов запроса и внутренних перенаправлений в контейнере

    Анализ логов пода: поиск следов запроса и внутренних перенаправлений в контейнере

    Вы отправляете curl на /reservation, мгновенно получаете ответ HTTP 404, после чего открываете логи пода aeqs-backend-preregistration и... не видите ничего. Ни ошибок, ни предупреждений, ни следов вашего запроса. Тишина в логах часто сбивает с толку: кажется, что запрос вообще не дошел до контейнера и потерялся где-то в сетях Kubernetes. Но мы уже знаем, что соседний метод /time_slots работает, а значит, сеть настроена верно.

    Причина тишины кроется в том, как Spring Boot по умолчанию относится к ошибкам маршрутизации.

    Иллюзия пустого лога

    В Spring Boot по умолчанию установлен уровень логирования INFO. Ошибки класса (включая наш 404 Not Found) считаются «клиентскими». С точки зрения фреймворка, приложение работает штатно: клиент запросил несуществующий ресурс, фреймворк корректно ответил отказом. Писать об этом в лог на уровне INFO — значит засорять вывод мусором при каждом сканировании портов или опечатке пользователя.

    Поэтому, если DispatcherServlet не находит подходящего метода (например, из-за отсутствия жестко требуемого параметра userSession), он просто возвращает 404, не оставляя следов в стандартном выводе.

    Чтобы увидеть реальную картину, нам нужно заглянуть под капот маршрутизатора.

    Динамическое переключение уровней логирования

    Перезапускать под с новыми переменными окружения для включения дебаг-логов — плохая идея. Это долго, к тому же перезапуск может сбросить текущее состояние приложения.

    Здесь на помощь снова приходит Spring Boot Actuator. Помимо просмотра маршрутов, он позволяет менять уровни логирования «на лету», без перезагрузки приложения. Нам нужно повысить детализацию для пакета org.springframework.web, который отвечает за HTTP-маршрутизацию.

    Выполняем POST-запрос к Actuator (если он открыт в DEV-контуре):

    Теперь Spring Boot начнет писать в лог каждый шаг обработки входящего HTTP-запроса.

    Поиск своего запроса в потоке данных

    Как только вы включите уровень TRACE, логи пода превратятся в водопад текста, так как туда попадут запросы от всех пользователей, автотестов и систем мониторинга (health checks). Искать свой запрос глазами бессмысленно.

    Нужно использовать уникальные маркеры. Посмотрим на исходный запрос из нашего автотеста:

    Заголовок X-Mdm-Id: 5300767615 — идеальный кандидат на роль идентификатора корреляции (Correlation ID). Если логирование в проекте настроено грамотно (например, через MDC — Mapped Diagnostic Context), этот ID будет прикреплен к каждой строке лога, относящейся к нашему запросу.

    Отправляем проблемный запрос на /reservation еще раз и фильтруем логи пода:

    Если MDC не настроен, мы можем искать по самому URL: grep "/reservation".

    !Как подтвердить, что 404 отдает именно Spring Boot

    Анатомия отказа: что мы ищем в логах

    Найдя строки, относящиеся к нашему запросу, мы можем увидеть один из двух сценариев, объясняющих 404 ошибку.

    Сценарий 1: Отказ на уровне DispatcherServlet

    Если проблема действительно в отсутствии параметра userSession (как мы подозревали при анализе Swagger), лог TRACE покажет процесс перебора маршрутов:

    Здесь мы явно видим, что запрос дошел до ядра Spring, DispatcherServlet попытался найти метод, но матрица условий (Handler Mapping) не сошлась. Это железобетонное доказательство того, что маршрут требует дополнительных данных (параметров или заголовков), которых нет в нашем curl.

    Сценарий 2: Перехват до маршрутизации (Фильтры)

    Возможна ситуация, когда DispatcherServlet даже не начинает работу. Запрос может быть отвергнут еще на подлете.

    Перед тем как запрос попадает в контроллер, он проходит через цепочку фильтров (Filter Chain). Это излюбленное место для логики безопасности. Если метод /reservation требует авторизации, а /time_slots — нет, то фильтр безопасности может проверить наличие сессии (того самого userSession) или токена. Не найдя его, фильтр прерывает цепочку.

    > Часто в целях безопасности системы возвращают 404 Not Found вместо 401 Unauthorized или 403 Forbidden, чтобы не раскрывать злоумышленникам факт существования защищенного эндпоинта.

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

    Сборка картины воедино

    Анализ логов с включенным дебагом — это финальный этап расследования внутри контейнера. Мы прошли путь от проверки Ingress и Context Path до чтения контрактов и, наконец, увидели пошаговое выполнение нашего запроса в памяти Spring Boot.

    Теперь у нас на руках есть все факты: мы знаем, как формируется URL, видим расхождение между Swagger и реальностью, и можем доказать по логам, в какой именно момент приложение отбрасывает запрос к /reservation. Осталось лишь синтезировать эти данные в готовое решение.

    7. Алгоритм исправления AEQS-49: синтез полученных знаний для восстановления работоспособности теста

    Алгоритм исправления AEQS-49: синтез полученных знаний для восстановления работоспособности теста

    Автотест AEQS-49 успешно проходит первый шаг, получая 200 OK от /time_slots, но спотыкается о глухую стену 404 Not Found при попытке создать бронирование через POST /reservation. У нас есть живой под, доступный Swagger, учтённый CONTEXT_PATH и полное отсутствие зацепок в стандартных логах. Разрозненные инструменты диагностики, которые мы применяли ранее, по отдельности дают лишь фрагменты картины.

    Чтобы окончательно починить тест и дать аргументированный ответ бэкенд-команде, необходимо свести проверку сети, анализ контрактов и динамическое логирование в единый, строгий алгоритм.

    Шаг 1. Отсечение сетевого слоя и Ingress

    Первое правило отладки — локализовать уровень отказа. В нашем случае этот шаг уже выполнен самой логикой теста.

    Успешный вызов /aeqs-backend-preregistration/v1/api/time_slots математически доказывает три факта:

  • Ingress-контроллер корректно маршрутизирует внешний трафик в нужный сервис Kubernetes.
  • Под aeqs-backend-preregistration жив, принимает соединения и не блокирует их на уровне сетевых политик.
  • Префикс CONTEXT_PATH определён верно.
  • > Если один эндпоинт приложения отвечает 200 OK, а соседний с идентичной базовой структурой пути возвращает 404 Not Found, проблема гарантированно находится внутри логики маршрутизации самого Spring Boot.

    Сетевой слой исключён. Мы спускаемся на уровень внутреннего устройства приложения.

    Шаг 2. Динамический перехват запроса в логах

    Поскольку Spring Boot по умолчанию скрывает детали 404 ошибок на уровне INFO, нам нужно «подсветить» путь нашего зависшего запроса. Мы не можем перезапускать под с новыми переменными окружения, поэтому используем горячее переключение уровней логирования.

    Выполняем запрос к Actuator для включения максимальной детализации веб-слоя:

    Сразу после этого запускаем падающий шаг автотеста (вызов /reservation). В логах пода aeqs-backend-preregistration появится огромный массив данных. Чтобы найти именно наш запрос среди фонового шума кластера, используем уникальный идентификатор корреляции, который тест уже передаёт в заголовках: X-Mdm-Id: 5300767615.

    Ищем этот ID в логах. Здесь нас ждут два варианта развития событий, которые определят дальнейшую судьбу теста.

    Вариант А: Ошибка строгой маршрутизации (Handler Mapping) В логах мы видим, что DispatcherServlet принял запрос, но отклонил его с формулировкой вроде Unsatisfied dependency или UnsatisfiedRequestParameterException. Это означает, что контроллер для /reservation существует, но Spring отказался передавать ему запрос из-за несовпадения условий. Это прямо указывает на гипотезу с параметром userSession.

    Вариант Б: Полное отсутствие маршрута (No Handler Found) В логах фиксируется No mapping for POST /v1/api/reservation. Spring честно признаётся, что внутри этого микросервиса вообще нет кода, способного обработать такой URL. Это подтверждает гипотезу о том, что логика бронирования переехала в другой сервис (например, aeqs-ticket), а Swagger просто не обновили.

    Шаг 3. Верификация через карту маршрутов (Actuator)

    Логи показали нам следствие. Теперь нужно найти причину в самом коде запущенного приложения, не имея доступа к исходникам. Обращаемся к источнику истины — эндпоинту /actuator/mappings.

    Запрашиваем карту маршрутов и ищем в JSON-ответе упоминание reservation:

    !Какой вывод делаем из результатов проверки маршрутов?

    Этот шаг ставит финальную точку в расследовании.

    Если в выгрузке Actuator мы находим блок для /reservation, и в нём указано details.requestMappingConditions.params: ["userSession"], значит, бэкенд-разработчики реализовали строгую валидацию на уровне маршрутизатора. Без этого параметра Spring просто «не видит» эндпоинт.

    Если же выгрузка пуста, а поиск по слову reservation ничего не даёт, значит метод физически отсутствует в этом микросервисе.

    Шаг 4. Внесение исправлений и закрытие AEQS-49

    На основе собранных улик мы формируем два артефакта: исправление в коде автотеста и баг-репорт (или запрос на уточнение) для бэкенд-команды.

    Сценарий 1: Проблема в параметре userSession * Действие в тесте: Модифицируем HTTP-клиент в Java-коде теста. Добавляем генерацию объекта userSession (с полями id, created, phone и т.д.) и передаём его как URL Query-параметр при POST-запросе. * Сообщение команде: «Коллеги, метод /reservation требует обязательной передачи userSession в query-параметрах для успешного Handler Mapping (иначе отдаёт 404). Тест адаптирован. Просьба подтвердить, что такая строгая привязка к параметру в POST-запросе — это ожидаемое архитектурное поведение, а не побочный эффект аннотаций».

    Сценарий 2: Метод мигрировал в другой сервис * Действие в тесте: Меняем базовый URL для шага бронирования. Вместо /aeqs-backend-preregistration/... направляем запрос в /aeqs-ticket/v1/api/reservation (предварительно проверив этот путь аналогичным образом). * Сообщение команде: «Коллеги, автотест падал с 404. Анализ /actuator/mappings показал, что метод /reservation отсутствует в сервисе aeqs-backend-preregistration. Тест переключен на сервис aeqs-ticket. Обнаружен Contract Drift: Swagger сервиса preregistration всё ещё отображает удалённый метод. Завёл дефект на обновление OpenAPI спецификации».

    Применяя этот алгоритм, вы перестаёте угадывать причины сбоев. Вы опираетесь на железобетонные факты: сетевую доступность, трассировку логов по Correlation ID и фактическую карту маршрутов приложения. Это превращает отладку из хаотичного перебора параметров в предсказуемый инженерный процесс.