## MODIFIED Requirements ### Requirement: Неизвестный путь вне корней открывает приложение Сервис SHALL отдавать разметку приложения на всяком пути, который не принадлежит ни одному корню сервиса и не совпадает с отдельным адресом наблюдения. Корень у сервиса остался **один** — `/app` у приложения; отдельными адресами стоят `/health` и `/metrics`. Корней `/api` и `/_` в перечне больше нет: встроенного хранилища с его собственным пространством и панели администратора у сервиса не осталось, и адресов под этими именами не существует. Прежние пути хранилища и панели поэтому отвечают тем же, чем отвечает всякий путь вне корней, — разметкой приложения. Резервировать имя за отказом сервис не берётся: имя, за которым ничего не стоит, ничем не отличается от любого другого свободного имени, а второй перечень «когда-то занятых корней» разошёлся бы с первым молча. Этим же снимается дефект подменённого знака: путь панели, записанный кодом знака, раскодируется в тот же путь и попадает в то же правило — правило одно, и особого случая у него нет. Корня `/auth` в перечне нет тоже: собственного входа у сервиса не осталось. Путь принадлежит корню, когда **совпадает с ним точно либо начинается им вместе с косой чертой**. Оба условия обязательны: по одному лишь префиксу корню `/app` достался бы посторонний `/apple`, а по одному лишь префиксу с косой чертой голый `/app` не достался бы никому и уехал бы разметкой. Чем отвечает голый `/app`, названо прямо: он принадлежит корню приложения, адресом приложения при этом не является и потому MUST отвечать как **неизвестный путь под корнем приложения** — узнанному `404` телом отказа приложения, неузнанному `401` тем же телом, каким отвечают прочие адреса под этим корнем. Разметки в ответе нет ни в одном из двух случаев. Без этой строки «не разметка приложения» читается как «что-нибудь ещё», и код ответа выбрала бы за нас первая же сборка. Путь, принадлежащий корню, MUST не проваливаться в приложение никогда: отказ контракта остаётся отказом контракта и уходит той формой, которой этот корень отвечает и сегодня. Иначе программа, ошибшаяся адресом под корнем приложения, получила бы разметку с кодом `200` вместо отказа с машиночитаемым кодом — и приняла бы её за ответ. Путь **под каталогом ресурсов** — тем, который наполняет сборщик, — разметкой не подменяется: не совпавший с файлом, он MUST отвечать `404`. Иначе разметка прежней сборки, назвавшая ресурс, которого в новой сборке уже нет, получает на него `200` и разметку вместо ресурса: браузер отвергнет её по типу содержимого, человек увидит пустой экран, а в кодах ответов сервиса не останется ничего. Открывающими страницу считаются `GET` и `HEAD`, и только они; прочие методы MUST отвечать `405`. #### Scenario: Обновление страницы посреди приложения открывает тот же экран - **GIVEN** приложение открыто на своём маршруте - **WHEN** браузер спрашивает этот путь заново - **THEN** ответ имеет код `200` - **AND** тело ответа — разметка приложения #### Scenario: Голый корень разметкой не подменяется - **GIVEN** запрос идёт с заголовком, поставленным прокси - **WHEN** запрос приходит на путь, совпадающий с корнем приложения точно и без косой черты - **THEN** ответ имеет код `404` - **AND** тело ответа — отказ приложения с машиночитаемым кодом, а не разметка #### Scenario: Голый корень неузнанному отвечает как прочие адреса под корнем - **WHEN** запрос приходит без заголовка на путь, совпадающий с корнем приложения точно и без косой черты - **THEN** ответ имеет код `401` - **AND** тело ответа — не разметка приложения #### Scenario: Посторонний путь, начинающийся именем корня, открывает приложение - **WHEN** запрос приходит на путь, который начинается именем корня, но не отделён от него косой чертой - **THEN** ответ имеет код `200` - **AND** тело ответа — разметка приложения #### Scenario: Прежний адрес входа открывает приложение - **WHEN** запрос приходит на путь под прежним корнем входа - **THEN** ответ имеет код `200` - **AND** тело ответа — разметка приложения #### Scenario: Прежний путь хранилища открывает приложение - **WHEN** запрос приходит на путь под прежним корнем хранилища - **THEN** ответ имеет код `200` - **AND** тело ответа — разметка приложения #### Scenario: Прежний адрес панели открывает приложение - **WHEN** запрос приходит на прежний адрес панели — и записанный знаком, и записанный кодом этого знака - **THEN** оба ответа имеют код `200` - **AND** тело каждого — разметка приложения #### Scenario: Неизвестный путь под корнем приложения отвечает отказом - **GIVEN** запрос идёт с заголовком, поставленным прокси - **WHEN** он спрашивает неизвестный путь под корнем приложения - **THEN** ответ имеет код `404` - **AND** тело ответа — отказ приложения с машиночитаемым кодом, а не разметка #### Scenario: Неизвестный путь под корнем приложения неузнанному отвечает как все прочие его адреса - **WHEN** запрос приходит на неизвестный путь под корнем приложения без заголовка - **THEN** ответ имеет код `401` - **AND** тело ответа — не разметка приложения #### Scenario: Несуществующий ресурс отвечает отсутствием, а не разметкой - **WHEN** браузер спрашивает под каталогом ресурсов файл, которого в сборке нет - **THEN** ответ имеет код `404` - **AND** тело ответа — не разметка приложения ### Requirement: Путь, отданный приложению, в журнал не идёт Сервис SHALL записывать о запросе **маршрут из закрытого перечня** и длину пути, а самого запрошенного пути MUST не записывать ни в один свой журнал. То же относится к меткам метрик. Перечень маршрутов закрыт и назван: точные адреса наблюдения и объявленные образцы адресов приложения; всё, что ни одному из них не отвечает, MUST обозначаться одним общим значением. Для запроса, отданного приложению, к строке добавляется **исход из закрытого перечня** — разметка, ресурс, отказ. Правило MUST накрывать обе половины адресного пространства — и путь вне корней сервиса, и путь под корнем приложения. Принадлежность пути сервису тут ничего не меняет: `/app/<произвольный текст>` принадлежит сервису и отвечает отказом, но множеством значений под корнем распоряжается спрашивающий ровно так же, как и вне его. Причина в том, кто этот путь выбирает. До появления раздачи путь вне корней ловил отказ маршрутизатора; теперь он успешный ответ, и множеством его значений распоряжается спрашивающий — дословная запись сделала бы журнал местом, куда аноним пишет свой текст произвольной длины. Правило того же рода у сервиса уже есть: причина отказа, пришедшая от провайдера строкой запроса, приводится к перечню известных. Идентификатор записи от этого не пропадает: его пишет обработчик своим полем, и пишет он прочитанный идентификатор, а не тот, что стоял в запросе. Журнал у сервиса теперь **один**: второй, куда чужая библиотека клала путь целиком вместе с адресом отправителя, ушёл вместе с ней. Правило от этого не ослабло, а перестало зависеть от настройки чужого журнала, которую мы не писали. #### Scenario: Путь не доезжает до журнала - **WHEN** приходит запрос на путь вне корней сервиса - **THEN** записи о нём не несут этого пути - **AND** несут исход и длину пути #### Scenario: Длинный путь журнал не наполняет - **WHEN** приходит запрос на путь длиной в тысячу знаков - **THEN** записи о нём не растут вместе с длиной пути #### Scenario: Путь под корнем приложения журнал не пишет - **GIVEN** пришедший не узнан - **WHEN** он спрашивает под корнем приложения путь, не отвечающий ни одному объявленному образцу адреса - **THEN** записи о нём не несут этого пути - **AND** не растут вместе с его длиной #### Scenario: Объявленный образец адреса приложения в журнале различим - **WHEN** приходит запрос на объявленный адрес приложения - **THEN** запись о нём несёт образец этого адреса, а не запрошенный путь #### Scenario: Второго журнала у сервиса нет - **GIVEN** сервис поднялся - **WHEN** приходит запрос на путь вне корней сервиса - **THEN** запись о нём появляется только в журнале сервиса