> For the complete documentation index, see [llms.txt](https://docs.ipcheck.ing/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ipcheck.ing/developer/ru/architecture/backend.md).

# Бэкенд

Бэкенд — это одно приложение Express 5. `backend-server.js` файл в корне репозитория — единственный, который связывает маршруты; каждый маршрут делегирует ровно одному модулю-обработчику в `api/`. Общий бэкенд-код находится в `common/`.

Общее правило: **`backend-server.js` определяет, кто может вызывать маршрут и как долго ответ можно кэшировать; обработчик только получает и преобразует данные.**

## Цепочка middleware по порядку

```mermaid
flowchart TD
    R["Входящий запрос"] --> P["pino-http на /api  (только когда LOG_HTTP=true)"]
    P --> RL["ограничитель запросов  (только когда SECURITY_RATE_LIMIT задан)"]
    RL --> SD["замедление  (только когда SECURITY_DELAY_AFTER задан)"]
    SD --> J["express.json, лимит 500 КБ"]
    J --> NS["Cache-Control: no-store в каждом ответе /api"]
    NS --> CA["cacheable(maxAge) — только на маршрутах, которые подключают это"]
    CA --> RF["requireReferer — глобально на /api/*"]
    RF --> G["Проверка параметров для каждого маршрута"]
    G --> H["Обработчик в api/"]
```

Каждый шаг стоит знать:

1. **`pino-http`** монтируется на `/api` только когда `LOG_HTTP=true`. Он стоит перед ограничителем запросов, так что 429 тоже логируются. Обработчики сами никогда не пишут строки "received request". См. [Логирование](/developer/ru/configuration/logging.md).
2. **Ограничитель запросов** (`express-rate-limit`): окно в 20 минут с `SECURITY_RATE_LIMIT` в качестве лимита; отключено, когда значение `0` или не задано. В момент точного перехода в состояние ограничения оно пишет одну `logger.warn({ ip }, 'IP rate-limited')` строку — не по одной на каждый заблокированный запрос — и при необходимости добавляет запись в журнал на диске, когда `SECURITY_BLACKLIST_LOG_FILE_PATH` задано. IP клиента берётся из `cf-connecting-ip`, затем первого `x-forwarded-for` значения, затем `cf-connecting-ipv6`, затем `req.ip` (`trust proxy` является `1`).
3. **Замедление** (`express-slow-down`): окно в 1 час, которое добавляет `попаданий × 400 мс` задержки после `SECURITY_DELAY_AFTER` запросов; отключено, когда не задано. Оба ограничителя пропускают `/monitoring`, у которого свой ограничитель — замедленный телеметрический туннель молча убивает отправку отчётов об ошибках.
4. **`express.json({ limit: '500kb' })`** — увеличен с дефолтных 100 КБ, потому что общие диагностические отчёты вполне достигают \~100 КБ. Он должен оставаться выше `REPORT_MAX_BYTES` в `common/report-schema.js`, иначе загрузка отчёта здесь падает с необработанным 413 ещё до собственной проверки размера обработчиком.
5. **`no-store` по умолчанию** на каждом `/api/*` ответе.
6. **`cacheable(maxAge)`** где маршрут включил это — см. [Кэширование на границе](#edge-caching).
7. **`requireReferer`**, глобально на `/api/*`.
8. **Проверки параметров для каждого маршрута** с `common/guards.js`.
9. **Обработчик.**

Переменные окружения, связанные с безопасностью, описаны в [Параметры безопасности](/developer/ru/configuration/security-options.md) и [Переменные окружения](/developer/ru/reference/environment-variables.md).

## Проверки

Контроль доступа и проверка параметров живут в middleware, никогда внутри обработчика. Всё это находится в `common/guards.js` и подключается в `backend-server.js`, так что обработчик может считать, что его входные данные уже корректны.

| Проверка                   | Проверки                                                                                                                                       | При сбое                                                                                 |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `requireReferer`           | Промежуточный слой `Referer` hostname — `localhost` или в `ALLOWED_DOMAINS`. Непарсящийся referer считается отклонённым.                       | `403` `{ error: 'Access denied' }`или `'Что вы делаете?'` когда referer не был отправлен |
| `requireValidIP()`         | `?ip=` присутствует и является корректным адресом IPv4/IPv6                                                                                    | `400` `IP-адрес не указан` / `Некорректный IP-адрес`                                     |
| `requireValidDomain()`     | `?domain=` является синтаксически корректным доменом. **Приводит его к нижнему регистру на месте** чтобы edge-кэш видел один канонический ключ | `400` `Домен не указан` / `Некорректный домен`                                           |
| `requireValidPrefix()`     | `?prefix=` является корректным CIDR. Политика квантования остаётся задачей фронтенда                                                           | `400` `Префикс не указан` / `Некорректный префикс`                                       |
| `requireValidASN()`        | `?asn=` числовой, опционально `AS`с префиксом -. **Переписывает его в числовую форму**                                                         | `400` `ASN не указан` / `Некорректный ASN`                                               |
| `requireValidProviderId()` | `?id=` является известным slug провайдера статуса сервиса                                                                                      | `400` `ID провайдера не указан` / `Некорректный ID провайдера`                           |
| `requireValidReportId()`   | Промежуточный слой `:id` **параметр маршрута** соответствует 22 символам base64url (16 случайных байт)                                         | `400` `Некорректный ID отчёта`                                                           |

Новый формат параметра означает новый гард в `common/guards.js` подключено в `backend-server.js` — а не встроенная проверка в обработчике. (`cf-radar` предшествует `requireValidASN()` и до сих пор валидирует свой ASN прямо в коде.) Проверки покрываются `tests/guards.test.js`.

## Форма обработчика

Каждый файл в `api/` имеет один default-экспорт, `async (req, res) => …`, который читает `req.query` или `req.body`, вызывает upstream и записывает ровно один ответ. Каждый файл начинается с комментария-заголовка, называющего его маршрут и назначение.

Формат ошибки намеренно лаконичен — фронтенд не показывает эти строки дословно:

```js
res.status(500).json({ error: error.message });  // сбой upstream
res.status(400).json({ error: 'Invalid …' });    // некорректный ввод (обычно проверка)
```

Некоторые обработчики сохраняют защитную `req.method !== 'GET'` ветку, возвращающую `405` хотя маршрут уже ограничивает метод, потому что smoke-тесты проверяют именно эту ветку напрямую.

Пять обработчиков источников IP-геолокации имеют ещё более тонкую структуру: они собираются с помощью `makeGeoHandler({ name, buildUrl, normalize })` в `common/geo-handler.js`, который отвечает за fetch, проверку на non-2xx, вызов нормализации и единообразный catch с логированием и 500. См. [Источники IP-данных](/developer/ru/architecture/ip-data-sources.md).

## Вызовы upstream

Каждый исходящий HTTP-запрос из `api/` проходит через `fetchUpstream` с `common/fetch-with-timeout.js`. Никогда не используйте голый `fetch()` или `https.get()` — зависший провайдер должен завершаться по тайм-ауту, а не удерживать соединение.

* **8-секундный тайм-аут** по умолчанию (сосед на стороне браузера, `fetchWithTimeout`, по умолчанию 5 с). Оба принимают `timeoutMs` переопределение и цепочку переданного вызывающим `signal`. Тайм-ауты проявляются как `AbortError`.
* **User-Agent проекта** из `MyIP/v<version>/<VITE_SITE_URL>`, регистрируемый при запуске через `common/upstream-ua.js`. Некоторые WAF upstream жёстко блокируют стандартный `User-Agent: node`. Форки указывают свой собственный `VITE_SITE_URL`.
* **Переданный вызывающим `User-Agent` заголовки всегда имеют приоритет**, включая `{ ...req.headers }` pass-through, описанный ниже.

{% hint style="info" %}
**Передача заголовков для private API.** Обработчики, которые проксируют private IPCheck.ing API — `ipcheck-ing`, `invisibility-test`, `update-user-achievement`, `get-user-info`, `dns-leak-test` — передают заголовки вызывающего наверх, потому что этому API нужен контекст вызывающего (`Accept-Language`, токены аутентификации). Это намеренное исключение. Сторонние upstream получают только то, что им явно нужно.
{% endhint %}

## Кэширование на границе

Каждый `/api/*` ответ изначально имеет `Cache-Control: no-store`. Медленно меняющиеся публичные маршруты подключают это через `cacheable(maxAgeSeconds)` фабрику middleware, определённую в `backend-server.js`:

```js
const cacheable = (maxAgeSeconds) => (req, res, next) => {
    res.locals.cacheControl = `public, max-age=${maxAgeSeconds}`;
    const originalJson = res.json.bind(res);
    res.json = function (body) {
        if (res.statusCode < 400) {
            res.setHeader('Cache-Control', res.locals.cacheControl);
        }
        return originalJson(body);
    };
    next();
};
```

Важны два следствия. Он перехватывает `res.json`, так что заголовок попадает только в ответы со статусом ниже 400 — CDN никогда не кэширует страницу ошибки. И он сохраняет нужное значение в `res.locals.cacheControl`, так что обработчики, которые стримят бинарные данные (обходя `res.json`), могут применить его сами на своём пути 2xx. В остальном обработчики никогда не трогают `Cache-Control`.

Используемые сейчас уровни TTL:

| TTL     | Маршруты                                                                                                                                                 | Почему                                                                                                                        |
| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| 5 минут | `/api/service-status`, `/api/service-status/detail`                                                                                                      | Соответствует собственному 5-минутному обновлению фонового опросчика                                                          |
| 1 час   | `/api/configs`                                                                                                                                           | Флаги функций, полученные из переменных окружения; они меняются при повторном развёртывании                                   |
| 1 день  | `/api/ipinfo`, `/api/ipapicom`, `/api/ipsb`, `/api/ipapiis`, `/api/ip2location`, `/api/maxmind`, `/api/whois`, `/api/github-stars`, `/api/ooni-blocking` | Данные геолокации и реестров почти не меняются в течение дня; к тому же это вежливо по отношению к бесплатным квотам upstream |
| 7 дней  | `/api/globalping-probes`                                                                                                                                 | Покрытие стран у проб меняется медленно, а селекторы работают по принципу fail open                                           |
| 30 дней | `/api/cfradar`, `/api/asn-history`, `/api/asn-connectivity`, `/api/macchecker`                                                                           | Реестровые и исторические данные: назначения IEEE OUI, метаданные ASN и взаимосвязи, append-only история маршрутизации BGP    |
| 1 год   | `/api/map`                                                                                                                                               | Статический тайл карты для квантизированной координаты                                                                        |

TTL записываются как выражения умножения (`24 * 60 * 60`), а не как голые секунды.

Всё остальное остаётся `no-store`: `/api/ipchecking`, `/api/dnsresolver`, `/api/dnsleaktest/session/:token`, `/api/invisibility`, `/api/getuserinfo`, `PUT /api/updateuserachievement`, и оба `/api/report` эндпоинта.

{% hint style="warning" %}
Никогда не оборачивайте аутентифицированный или пользовательский эндпоинт в `cacheable()`. Его кэширование относится к upstream, который владеет контекстом аутентификации. Общие отчёты остаются `no-store` по ещё одной причине: edge-кэш может отдать отчёт после истечения срока в KV, а частные диагностические данные не должны попадать в публичный кэш.
{% endhint %}

## Краткий обзор маршрутов

Полный контракт находится в [Конечные точки API](/developer/ru/reference/api-endpoints.md); это вид связей.

| Маршрут                               | Проверка                          | Кэш      | Обработчик                       |
| ------------------------------------- | --------------------------------- | -------- | -------------------------------- |
| `GET /api/ipinfo`                     | `requireValidIP()`                | 1 день   | `api/ipinfo-io.js`               |
| `GET /api/ipapicom`                   | `requireValidIP()`                | 1 день   | `api/ipapi-com.js`               |
| `GET /api/ipapiis`                    | `requireValidIP()`                | 1 день   | `api/ipapi-is.js`                |
| `GET /api/ip2location`                | `requireValidIP()`                | 1 день   | `api/ip2location-io.js`          |
| `GET /api/ipsb`                       | `requireValidIP()`                | 1 день   | `api/ip-sb.js`                   |
| `GET /api/maxmind`                    | `requireValidIP()`                | 1 день   | `api/maxmind.js`                 |
| `GET /api/ipchecking`                 | `requireValidIP()`                | no-store | `api/ipcheck-ing.js`             |
| `GET /api/whois`                      | —                                 | 1 день   | `api/get-whois.js`               |
| `GET /api/macchecker`                 | —                                 | 30 дней  | `api/mac-checker.js`             |
| `GET /api/dnsresolver`                | —                                 | no-store | `api/dns-resolver.js`            |
| `GET /api/cfradar`                    | встроенная проверка ASN           | 30 дней  | `api/cf-radar.js`                |
| `GET /api/asn-history`                | `requireValidPrefix()`            | 30 дней  | `api/asn-history.js`             |
| `GET /api/asn-connectivity`           | `requireValidASN()`               | 30 дней  | `api/asn-connectivity.js`        |
| `GET /api/ooni-blocking`              | `requireValidDomain()`            | 1 день   | `api/ooni-blocking.js`           |
| `GET /api/globalping-probes`          | —                                 | 7 дней   | `api/globalping-probes.js`       |
| `GET /api/service-status`             | —                                 | 5 мин    | `api/service-status.js`          |
| `GET /api/service-status/detail`      | `requireValidProviderId()`        | 5 мин    | `api/service-status.js`          |
| `GET /api/map`                        | —                                 | 1 год    | `api/google-map.js`              |
| `GET /api/github-stars`               | —                                 | 1 день   | `api/github-stars.js`            |
| `GET /api/configs`                    | —                                 | 1 час    | `api/configs.js`                 |
| `GET /api/invisibility`               | —                                 | no-store | `api/invisibility-test.js`       |
| `GET /api/dnsleaktest/session/:token` | —                                 | no-store | `api/dns-leak-test.js`           |
| `GET /api/getuserinfo`                | —                                 | no-store | `api/get-user-info.js`           |
| `PUT /api/updateuserachievement`      | —                                 | no-store | `api/update-user-achievement.js` |
| `POST /api/report`                    | —                                 | no-store | `api/share-report.js`            |
| `GET /api/report/:id`                 | `requireValidReportId()`          | no-store | `api/share-report.js`            |
| `POST /api/monitoring`                | собственный ограничитель запросов | no-store | `api/sentry-tunnel.js`           |

`/api/monitoring` монтируется **только** when `VITE_SENTRY_DSN_FRONTEND` задано. Он использует `express.raw({ type: () => true })` — универсальную функцию, потому что Replay-конверты бинарные и приходят без `Content-Type` вообще, чего `'*/*'` строковый сопоставитель пропустил бы.

## Последовательность запуска

`bootBackend()` подготавливает каждый офлайн-датасет **до** до открытия слушателя, так что сервер никогда не отдаёт наполовину загруженную базу данных:

1. `bootstrapMaxMindIfMissing()` затем `reloadMaxMindDatabases('startup')`
2. `bootstrapCaidaIfMissing()`
3. `bootstrapServiceStatus()`
4. Запускает наблюдатель за файлами MaxMind, автообновляторы MaxMind и CAIDA, а также опросчик статуса сервисов
5. `app.listen(BACKEND_PORT)`

Каждый шаг не является фатальным. Сбой оставляет зависимый API в деградированном состоянии — MaxMind отвечает `503`, представления на основе CAIDA возвращают пустой граф или откатываются к RIPEstat — но это никогда не блокирует запуск. Подробности о датасетах находятся в [Источники IP-данных](/developer/ru/architecture/ip-data-sources.md).

## Логирование и мониторинг ошибок

Файлы бэкенда всегда используют общий logger pino из `common/logger.js`; голый `console.*` там не используется. Pino ставит контекст на первое место: `logger.error({ err, ip }, 'short message')`. Строки запуска начинаются с эмодзи (🚀 прослушивание, 📦 готово, 📥 загрузка, 🛡️ безопасность, 🐢 замедление, 🗓️ расписание, ⚠️ восстановимо, ❌ сбой); строки по запросам остаются обычными.

Sentry включается через переменные окружения и невидим для обработчиков. `sentry-instrument.js` загружается через `node --import` **до** Express, чтобы хуки ESM loader могли автоматически инструменировать трассировку маршрутов; `backend-server.js` подключает `setupExpressErrorHandler` после всех маршрутов. Без `SENTRY_DSN_BACKEND`, `@sentry/node` никогда не загружается. Обработчики никогда не импортируют Sentry: неперехваченные исключения и трассировки 5xx происходят автоматически, а пойманные сбои остаются в logger, где хук зеркалит warn и выше в Sentry Logs. Периодические задачи оборачивают свой тик в `common/sentry-cron.js` для check-in'ов, а query-параметры API-ключей удаляются из URL телеметрии через `common/sentry-scrub.js`.

## Добавление маршрута

1. Создайте `api/<name>.js` с комментарием-заголовком и единственным default-экспортом.
2. Используйте `fetchUpstream` для любого исходящего вызова.
3. Подключайте его в `backend-server.js`, в правильном уровне кэша, с нужными ему проверками.
4. Если форма параметра новая, добавьте проверку в `common/guards.js` сначала.
5. Добавьте smoke-тесты для `tests/api-handlers.test.js` — ограничения по методу, ветки параметров, ранние возвраты при отсутствии API-ключа. Никогда не обращайтесь к реальному upstream; проверяйте только ветки, которые возвращают ответ до первого `fetchUpstream`. См. [Тестирование](/developer/ru/development/testing.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.ipcheck.ing/developer/ru/architecture/backend.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
