> 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/fr/architecture/backend.md).

# Backend

Le backend est une seule application Express 5. `backend-server.js` à la racine du dépôt se trouve le seul fichier qui branche les routes ; chaque route délègue à exactement un module de gestionnaire sous `api/`. Le code back-end partagé se trouve dans `common/`.

Règle générale : **`backend-server.js` détermine qui peut appeler une route et combien de temps la réponse peut être mise en cache ; le gestionnaire ne fait que récupérer et formater les données.**

## Chaîne de middleware, dans l'ordre

```mermaid
flowchart TD
    R["Requête entrante"] --> P["pino-http sur /api  (uniquement lorsque LOG_HTTP=true)"]
    P --> RL["limiteur de débit  (uniquement lorsque SECURITY_RATE_LIMIT est défini)"]
    RL --> SD["ralentissement  (uniquement lorsque SECURITY_DELAY_AFTER est défini)"]
    SD --> J["express.json, limite de 500 Ko"]
    J --> NS["Cache-Control: no-store sur chaque réponse /api"]
    NS --> CA["cacheable(maxAge) — uniquement sur les routes qui l'activent"]
    CA --> RF["requireReferer — global sur /api/*"]
    RF --> G["Gardes de paramètres par route"]
    G --> H["Gestionnaire dans api/"]
```

Chaque étape mérite d'être connue :

1. **`pino-http`** se monte sur `/api` montée sur `LOG_HTTP=true`. Il se place avant le limiteur de débit afin que les 429 soient aussi journalisés. Les gestionnaires n'écrivent jamais eux-mêmes de lignes « requête reçue ». Voir [Journalisation](/developer/fr/configuration/logging.md).
2. **Limiteur de débit** (`express-rate-limit`) : une fenêtre de 20 minutes avec `SECURITY_RATE_LIMIT` comme plafond ; désactivé lorsque la valeur est `0` ou non définie. Lors du passage exact à l'état limité, il écrit une seule `logger.warn({ ip }, 'IP limitée par le débit')` ligne — pas une par requête bloquée — et ajoute éventuellement une entrée dans un registre sur disque lorsque `SECURITY_BLACKLIST_LOG_FILE_PATH` est défini. L'adresse IP du client est lue depuis `cf-connecting-ip`, puis le premier `x-forwarded-for` entrée, puis `cf-connecting-ipv6`, puis `req.ip` (`trust proxy` est `1`).
3. **Ralentissement** (`express-slow-down`) : une fenêtre d'une heure qui ajoute `requêtes × 400 ms` de délai après `SECURITY_DELAY_AFTER` requêtes ; désactivé lorsqu'il n'est pas défini. Les deux limiteurs ignorent `/monitoring`, qui possède son propre limiteur — un tunnel de télémétrie bridé tue silencieusement le reporting d'erreurs.
4. **`express.json({ limit: '500kb' })`** — relevée par rapport à la valeur par défaut de 100 Ko, car les rapports de diagnostic partagés atteignent légitimement environ 100 Ko. Elle doit rester au-dessus de `REPORT_MAX_BYTES` dans `common/report-schema.js`, sinon les envois de rapports échouent ici avec un 413 brut avant le propre contrôle de taille du gestionnaire.
5. **`no-store` par défaut** sur chaque `/api/*` réponse.
6. **`cacheable(maxAge)`** où une route l'a activé — voir [Mise en cache en périphérie](#edge-caching).
7. **`requireReferer`**, global sur `/api/*`.
8. **Gardes de paramètres par route** depuis `common/guards.js`.
9. **Le gestionnaire.**

Les variables d'environnement liées à la sécurité sont documentées dans [Options de sécurité](/developer/fr/configuration/security-options.md) et [Variables d'environnement](/developer/fr/reference/environment-variables.md).

## Gardes

Le contrôle d'accès et la validation des paramètres vivent dans le middleware, jamais à l'intérieur d'un gestionnaire. Tout cela se trouve dans `common/guards.js` et est branché dans `backend-server.js`, de sorte qu'un gestionnaire peut supposer que ses entrées sont déjà correctement formées.

| Garde                      | Vérifications                                                                                                                              | En cas d'échec                                                                                  |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| `requireReferer`           | Le `Referer` le nom d'hôte est `localhost` ou dans `ALLOWED_DOMAINS`. Un referer non analysable est considéré comme refusé.                | `403` `{ error: 'Accès refusé' }`, ou `'Que faites-vous ?'` lorsqu'aucun referer n'a été envoyé |
| `requireValidIP()`         | `?ip=` est présent et constitue une adresse IPv4/IPv6 valide                                                                               | `400` `Aucune adresse IP fournie` / `Adresse IP invalide`                                       |
| `requireValidDomain()`     | `?domain=` est un domaine syntaxiquement valide. **Le met en minuscules sur place** afin que le cache de bord voie une seule clé canonique | `400` `Aucun domaine fourni` / `Domaine invalide`                                               |
| `requireValidPrefix()`     | `?prefix=` est un CIDR correctement formé. La politique de quantification reste du ressort du frontend                                     | `400` `Aucun préfixe fourni` / `Préfixe invalide`                                               |
| `requireValidASN()`        | `?asn=` est numérique, éventuellement `préfixe AS`préfixé par -. **Le réécrit sous forme numérique**                                       | `400` `Aucun ASN fourni` / `ASN invalide`                                                       |
| `requireValidProviderId()` | `?id=` est un identifiant connu de fournisseur d'état de service                                                                           | `400` `Aucun identifiant de fournisseur fourni` / `Identifiant de fournisseur invalide`         |
| `requireValidReportId()`   | Le `:id` **paramètre de route** correspond à 22 caractères base64url (16 octets aléatoires)                                                | `400` `Identifiant de rapport invalide`                                                         |

Une nouvelle forme de paramètre signifie une nouvelle garde dans `common/guards.js` branché dans `backend-server.js` — et non un contrôle en ligne dans le gestionnaire. (`cf-radar` précède `requireValidASN()` et valide encore son ASN en ligne.) Les gardes sont couvertes par `tests/guards.test.js`.

## Forme du handler

Chaque fichier dans `api/` a une unique exportation par défaut, `async (req, res) => …`, qui lit `req.query` ou `req.body`, appelle le service amont et écrit exactement une réponse. Chaque fichier commence par un commentaire d'en-tête nommant sa route et son objectif.

La forme d'erreur est volontairement concise — le frontend n'affiche pas ces chaînes mot à mot :

```js
res.status(500).json({ error: error.message });  // échec du service amont
res.status(400).json({ error: 'Invalid …' });    // entrée invalide (généralement une garde)
```

Certains gestionnaires conservent une `req.method !== 'GET'` branche renvoyant `405` même si la route bloque déjà cette méthode, car les tests de fumée affirment directement cette branche.

Les cinq gestionnaires sources de géolocalisation IP partagent une forme encore plus légère : ils sont construits par le `makeGeoHandler({ name, buildUrl, normalize })` dans `common/geo-handler.js`, qui prend en charge le fetch, la vérification des non-2xx, l'appel de normalisation et le catch uniforme de journalisation et 500. Voir [Sources de données IP](/developer/fr/architecture/ip-data-sources.md).

## Appels aux services amont

Chaque appel HTTP sortant depuis `api/` passe par `fetchUpstream` depuis `common/fetch-with-timeout.js`. Jamais un simple `fetch()` ou `https.get()` — un fournisseur bloqué doit expirer au lieu de monopoliser la connexion.

* **délai d'expiration de 8 secondes** par défaut (le pendant côté navigateur, `fetchWithTimeout`, est à 5 s par défaut). Les deux acceptent une `timeoutMs` de remplacement et chaînent un `signal`. Les expirations se manifestent sous forme de `AbortError`.
* **Un User-Agent de projet** de `MyIP/v<version>/<VITE_SITE_URL>`, enregistré au démarrage par `common/upstream-ua.js`. Certains WAF amont bloquent strictement le défaut d'undici `User-Agent : node`. Les forks annoncent leur propre `VITE_SITE_URL`.
* **Fourni par l'appelant `User-Agent` les en-têtes l'emportent toujours**, y compris le `{ ...req.headers }` réacheminement décrit ci-dessous.

{% hint style="info" %}
**Réacheminement des en-têtes pour l'API privée.** Les gestionnaires qui proxyfient l'API IPCheck.ing privée — `ipcheck-ing`, `invisibility-test`, `update-user-achievement`, `get-user-info`, `dns-leak-test` — transmettent les en-têtes de l'appelant au service amont, car cette API a besoin du contexte de l'appelant (`Accept-Language`, jetons d'authentification). Il s'agit d'une exception intentionnelle. Les services amont tiers ne reçoivent que ce dont ils ont explicitement besoin.
{% endhint %}

## Mise en cache en périphérie

Chaque `/api/*` la réponse commence par `Cache-Control: no-store`. Les routes publiques qui changent lentement s'activent via la `cacheable(maxAgeSeconds)` fabrique de middleware définie dans `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();
};
```

Deux conséquences importent. Il se branche sur `res.json`, donc l'en-tête n'apparaît que sur les réponses dont le statut est inférieur à 400 — un CDN ne met jamais en cache une page d'erreur. Et il stocke la valeur prévue dans `res.locals.cacheControl`, de sorte que les gestionnaires qui diffusent des données binaires (en contournant `res.json`) peuvent l'appliquer eux-mêmes sur leur propre chemin 2xx. Les gestionnaires ne touchent autrement jamais à `Cache-Control`.

Niveaux de TTL actuellement en usage :

| TTL       | Routes                                                                                                                                                   | Pourquoi                                                                                                                                              |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| 5 minutes | `/api/service-status`, `/api/service-status/detail`                                                                                                      | Correspond à l'actualisation de 5 minutes du poller en arrière-plan                                                                                   |
| 1 heure   | `/api/configs`                                                                                                                                           | Flags de fonctionnalité dérivés des variables d'environnement ; ils changent lors d'un redéploiement                                                  |
| 1 jour    | `/api/ipinfo`, `/api/ipapicom`, `/api/ipsb`, `/api/ipapiis`, `/api/ip2location`, `/api/maxmind`, `/api/whois`, `/api/github-stars`, `/api/ooni-blocking` | Les données de géolocalisation et de registre bougent à peine en une journée ; cela reste aussi poli vis-à-vis des quotas gratuits des services amont |
| 7 jours   | `/api/globalping-probes`                                                                                                                                 | La couverture des pays sondés change lentement et les sélecteurs échouent en mode ouvert                                                              |
| 30 jours  | `/api/cfradar`, `/api/asn-history`, `/api/asn-connectivity`, `/api/macchecker`                                                                           | Données de registre et historiques : attributions IEEE OUI, métadonnées et interconnexion ASN, historique de routage BGP en ajout uniquement          |
| 1 an      | `/api/map`                                                                                                                                               | Une tuile de carte statique pour une coordonnée quantifiée                                                                                            |

Les TTL sont écrits sous forme d'expressions multipliées (`24 * 60 * 60`), pas en secondes brutes.

Tout le reste demeure `no-store`: `/api/ipchecking`, `/api/dnsresolver`, `/api/dnsleaktest/session/:token`, `/api/invisibility`, `/api/getuserinfo`, `PUT /api/updateuserachievement`, et les deux `/api/report` points de terminaison.

{% hint style="warning" %}
N'enveloppez jamais un point de terminaison authentifié ou par utilisateur dans `cacheable()`. Son cache appartient au service amont qui possède le contexte d'authentification. Les rapports partagés restent `no-store` pour une deuxième raison : un cache de bord pourrait servir un rapport après l'expiration de sa KV, et les données de diagnostic privées n'ont pas leur place dans un cache public.
{% endhint %}

## Inventaire des routes en un coup d'œil

Le contrat complet se trouve dans [Points de terminaison API](/developer/fr/reference/api-endpoints.md); voici la vue de branchement.

| Route                                 | Garde                      | Cache    | Gestionnaire                     |
| ------------------------------------- | -------------------------- | -------- | -------------------------------- |
| `GET /api/ipinfo`                     | `requireValidIP()`         | 1 jour   | `api/ipinfo-io.js`               |
| `GET /api/ipapicom`                   | `requireValidIP()`         | 1 jour   | `api/ipapi-com.js`               |
| `GET /api/ipapiis`                    | `requireValidIP()`         | 1 jour   | `api/ipapi-is.js`                |
| `GET /api/ip2location`                | `requireValidIP()`         | 1 jour   | `api/ip2location-io.js`          |
| `GET /api/ipsb`                       | `requireValidIP()`         | 1 jour   | `api/ip-sb.js`                   |
| `GET /api/maxmind`                    | `requireValidIP()`         | 1 jour   | `api/maxmind.js`                 |
| `GET /api/ipchecking`                 | `requireValidIP()`         | no-store | `api/ipcheck-ing.js`             |
| `GET /api/whois`                      | —                          | 1 jour   | `api/get-whois.js`               |
| `GET /api/macchecker`                 | —                          | 30 jours | `api/mac-checker.js`             |
| `GET /api/dnsresolver`                | —                          | no-store | `api/dns-resolver.js`            |
| `GET /api/cfradar`                    | contrôle ASN en ligne      | 30 jours | `api/cf-radar.js`                |
| `GET /api/asn-history`                | `requireValidPrefix()`     | 30 jours | `api/asn-history.js`             |
| `GET /api/asn-connectivity`           | `requireValidASN()`        | 30 jours | `api/asn-connectivity.js`        |
| `GET /api/ooni-blocking`              | `requireValidDomain()`     | 1 jour   | `api/ooni-blocking.js`           |
| `GET /api/globalping-probes`          | —                          | 7 jours  | `api/globalping-probes.js`       |
| `GET /api/service-status`             | —                          | 5 min    | `api/service-status.js`          |
| `GET /api/service-status/detail`      | `requireValidProviderId()` | 5 min    | `api/service-status.js`          |
| `GET /api/map`                        | —                          | 1 an     | `api/google-map.js`              |
| `GET /api/github-stars`               | —                          | 1 jour   | `api/github-stars.js`            |
| `GET /api/configs`                    | —                          | 1 heure  | `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`                | propre limiteur de débit   | no-store | `api/sentry-tunnel.js`           |

`/api/monitoring` est monté **uniquement** when `VITE_SENTRY_DSN_FRONTEND` est défini. Il utilise `express.raw({ type: () => true })` — une fonction attrape-tout, car les enveloppes Replay sont binaires et arrivent sans `Content-Type` du tout, qu'un `'*/*'` concordeur de chaînes ignorerait.

## Séquence de démarrage

`bootBackend()` prépare chaque jeu de données hors ligne **avant** à l'ouverture de l'écouteur, afin que le serveur ne serve jamais une base de données à moitié téléchargée :

1. `bootstrapMaxMindIfMissing()` puis `reloadMaxMindDatabases('startup')`
2. `bootstrapCaidaIfMissing()`
3. `bootstrapServiceStatus()`
4. Démarre le surveillant de fichiers MaxMind, les auto-metteurs à jour MaxMind et CAIDA, ainsi que le poller d'état des services
5. `app.listen(BACKEND_PORT)`

Chaque étape n'est pas fatale. Un échec laisse l'API dépendante dégradée — MaxMind répond `503`, les vues alimentées par CAIDA renvoient un graphe vide ou retombent sur RIPEstat — mais cela ne bloque jamais le démarrage. Les détails des jeux de données se trouvent dans [Sources de données IP](/developer/fr/architecture/ip-data-sources.md).

## Journalisation et surveillance des erreurs

Les fichiers du backend utilisent toujours le logger pino partagé de `common/logger.js`; un simple `console.*` n'est pas utilisé ici. Pino place le contexte en premier : `logger.error({ err, ip }, 'court message')`. Les lignes de démarrage commencent par un emoji (🚀 écoute, 📦 prêt, 📥 téléchargement, 🛡️ sécurité, 🐢 limitation, 🗓️ planification, ⚠️ récupérable, ❌ échec) ; les lignes par requête restent simples.

Sentry est conditionné par les variables d'environnement et invisible pour les gestionnaires. `sentry-instrument.js` est chargé via `node --import` **avant** Express afin que les hooks du chargeur ESM puissent auto-instrumenter le traçage des routes ; `backend-server.js` attache `setupExpressErrorHandler` après toutes les routes. Sans `SENTRY_DSN_BACKEND`, `@sentry/node` n'est jamais chargé. Les gestionnaires n'importent jamais Sentry : les exceptions non interceptées et les traces 5xx sont automatiques, tandis que les échecs capturés restent dans le logger, où un hook réplique les niveaux warn et supérieurs vers Sentry Logs. Les tâches périodiques enveloppent leur tick dans `common/sentry-cron.js` pour les check-ins, et les paramètres de requête des clés API sont expurgés des URL de télémétrie par `common/sentry-scrub.js`.

## Ajouter une route

1. Créer `api/<name>.js` avec un commentaire d'en-tête et une unique exportation par défaut.
2. Utilisez `fetchUpstream` pour tout appel sortant.
3. Branchez-le dans `backend-server.js`, dans le bon niveau de cache, avec les gardes dont il a besoin.
4. Si la forme des paramètres est nouvelle, ajoutez une garde à `common/guards.js` d'abord.
5. Ajoutez des tests de fumée à `tests/api-handlers.test.js` — blocage par méthode, branches de paramètres, retours précoces en cas d'absence de clé API. N'appelez jamais un vrai service amont ; n'affirmez que les branches qui reviennent avant le premier `fetchUpstream`. Voir [Tests](/developer/fr/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/fr/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.
