## 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** запись о нём появляется только в журнале сервиса
