capitalisation: triage 95_a_capitaliser + création domaine infra

Triage des 27 propositions du buffer de capitalisation (skill
capitalisation-triage), avec vérification des doublons contre la base.

Intégré dans knowledge/ (23 entrées):
- backend: redis (compensation incrBy non-atomique), nestjs (injection
  cassée sous tsx watch; guard write mode dégradé), async (test rollback
  pipeline multi-fichiers), contracts (idempotence POST), auth (disclosure
  comptes soft-deleted), prisma (index partial soft-delete), llm-providers
  (nouveau: OAuth vs API key, prompt caching).
- frontend: tests (garde-fous parking Later), navigation (fichiers
  non-route sous src/app Expo Router), general (type client vs payload
  backend), state (fallback catch-all mapping DB→UI).
- workflow: story-tracking (statut BMAD vs narratif obsolète).
- product: general (nouveau: doc feature store sans UI).
- infra: NOUVEAU DOMAINE (traefik, tailscale, docker, docker-networking,
  reverse-proxy-paths, sidecar tailscale) + 00_INDEX.md.

Autres:
- 90_debug_et_postmortem.md: post-mortem réseau Docker partagé hors compose.
- Rejeté 3 doublons (types enum contracts, getter PrismaService, $transaction).
- Buffer 95_a_capitaliser.md purgé et restauré à son état initial.
- _projects.conf: MAJ statuts epics + ajout app-rl799.
This commit is contained in:
MaksTinyWorkshop
2026-06-25 10:31:22 +02:00
parent 1c876309f1
commit ef24d85d57
31 changed files with 1042 additions and 27 deletions
+13
View File
@@ -0,0 +1,13 @@
# Infra — Patterns validés — Index
Patterns d'infrastructure (Docker, reverse proxy, Tailscale, homelab NUC) testés et validés en conditions réelles.
Avant toute proposition infra, identifie le fichier dont le nom et la description matchent le domaine traité, puis lis-le.
---
| Fichier | Domaine | Entrées clés |
|---------|---------|--------------|
| `docker-networking.md` | Bridges Docker, communication container ↔ hôte | Bridges isolés / service hôte injoignable, conteneuriser plutôt que firewall, `network_mode: host`, anti-pattern `host-gateway` |
| `reverse-proxy-paths.md` | Servir une app sous un sous-chemin (Traefik) | App consciente du préfixe (stripPrefix ou non), chemins relatifs, app racine, conflits de path multi-apps |
| `tailscale.md` | Sidecar Tailscale pour app incompatible subpath | Quand l'utiliser, architecture, squelette compose, `serve.json`, init obligatoire, coûts |
@@ -0,0 +1,57 @@
---
title: Infra — Patterns validés : Docker networking
domain: infra
bucket: patterns
tags: [docker, networking, bridge, traefik, host-gateway, firewall]
applies_to: [architecture, implementation, debug]
severity: medium
validated_on: 2026-06-25
source_projects: [_Assistant_Cuisine]
---
# Infra — Patterns validés : Docker networking
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/infra/patterns/README.md` pour l'index complet.
---
<a id="pattern-bridge-isole-service-hote"></a>
## Bridges Docker isolés — un container ne peut pas joindre un service hôte par défaut
### Constat
Un container connecté à un bridge custom (ex. `traefik-net` en `172.21.0.0/16`) ne peut **pas** atteindre par défaut :
- l'IP hôte côté `docker0` (`172.17.0.1`)
- l'IP hôte sur d'autres interfaces (ex. IP Tailscale `100.x.x.x`)
- `host.docker.internal` (qui résout vers `172.17.0.1`, donc même blocage)
Même quand l'hôte écoute bien sur `0.0.0.0:PORT`, l'appel depuis le container part en timeout. Cause : sous Docker 29 + iptables/nftables strict, les bridges sont isolés entre eux et le forward bridge → host est bloqué par défaut.
**Conséquence** : faire pointer un router Traefik vers `host.docker.internal:PORT` pour atteindre un service systemd hôte ne fonctionne pas en bridge mode.
### Bonnes pratiques / solutions (par ordre de propreté)
**Solution 1 (préférée) — Conteneuriser le service hôte**
Mettre le service dans un container connecté au même bridge (`traefik-net`). La communication passe par le DNS Docker interne (`servicename:port`). Aucune complexité réseau, fonctionne nativement. C'est presque toujours possible (ex. code-server via l'image `lscr.io/linuxserver/code-server`).
**Solution 2 — Traefik en `network_mode: host`**
Traefik abandonne son bridge custom et écoute directement sur les interfaces hôte. Il atteint tous les services hôte trivialement, MAIS perd l'accès aux containers via bridge : il faut alors leur publier des ports hôte, ce qui défait l'intérêt du reverse proxy interne. Réservé aux setups simples où Traefik ne sert que des services hôte.
**Solution 3 — Ouvrir manuellement le firewall**
```bash
sudo iptables -I DOCKER-USER -s 172.21.0.0/16 -d 172.21.0.1 -p tcp --dport <PORT> -j ACCEPT
```
Fragile : règle volatile, à refaire au reboot ou à l'upgrade Docker. Si on prend ce chemin, documenter et provisionner via une unit systemd.
### Anti-pattern : `extra_hosts: host.docker.internal:host-gateway`
Cette ligne déclare seulement un alias DNS dans le `/etc/hosts` du container ; elle ne crée **aucune route réseau**. Si le firewall Docker bloque, l'alias ne sert à rien : le container résout `host.docker.internal` mais le SYN reste bloqué.
**Règle** : avant de partir sur la Solution 2 ou 3, toujours vérifier qu'on ne peut pas conteneuriser le service. C'est presque toujours possible et toujours plus propre.
- Contexte technique : Docker 29 / Traefik / NUC — _Assistant_Cuisine 04-05-2026
@@ -0,0 +1,57 @@
---
title: Infra — Patterns validés : Reverse proxy & sous-chemins
domain: infra
bucket: patterns
tags: [traefik, reverse-proxy, pathprefix, stripprefix, subpath, spa]
applies_to: [architecture, implementation, debug]
severity: medium
validated_on: 2026-06-25
source_projects: [_Assistant_Cuisine]
---
# Infra — Patterns validés : Reverse proxy & sous-chemins
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/infra/patterns/README.md` pour l'index complet.
---
<a id="pattern-app-sous-chemin-reverse-proxy"></a>
## Servir une app sous un sous-chemin via reverse proxy
Le pattern qui fonctionne dépend de la façon dont l'app gère ses URLs internes (assets, API, redirects). Trois cas.
### Cas 1 — App nativement consciente du préfixe
L'app prend une option de base path et émet ses URLs internes préfixées. Côté Traefik, lire la doc de l'app pour savoir si un `stripPrefix` est requis — il n'y a **pas de règle universelle** :
- **App qui reçoit le path complet et sait quoi en faire** (ex. Cooklang `cook server --url-prefix /cuisine`) : `PathPrefix(/cuisine)` **sans** `stripPrefix`.
- **App qui veut recevoir `/` mais émet ses assets sous le préfixe** (ex. Portainer `--base-url=/portainer`) : `PathPrefix(/portainer)` **avec** `stripPrefix /portainer`.
### Cas 2 — App qui sert uniquement des chemins relatifs
Si l'app utilise des URLs relatives partout (`<a href="./xxx">`, `<script src="./xxx">`, redirects relatifs aussi), elle fonctionne sous n'importe quel sous-chemin avec un simple `stripPrefix` côté Traefik. L'app ne sait pas où elle est, mais le navigateur résout correctement les URLs relatives (cas observé sur code-server).
⚠️ Vérifier qu'**aucune URL absolue** ne traîne dans le HTML/JS. Un `href="/static/..."` (au lieu de `href="./static/..."` ou `href="static/..."`) casse le rendu.
### Cas 3 — App qui veut être à la racine
Beaucoup de SPA modernes (Homepage, Vite/Next statiques) hardcodent `href="/_next/..."` etc. Le path prefix casse alors les assets. Solutions :
- Sous-domaine dédié (`homepage.example.com`) plutôt que sous-chemin.
- Sinon, plugin de réécriture de body (ex. `traefik/plugin-rewritebody`) qui patche le HTML à la volée.
- Si l'app est incompatible subpath ET doit rester derrière Tailscale : sidecar Tailscale dédié (voir `knowledge/infra/patterns/tailscale.md`).
**Règle** : tester en priorité avec un simple `PathPrefix + stripPrefix`. Si les assets cassent, lire la doc de l'app pour une option de base path. Si rien d'officiel, basculer sur sous-domaine (ou sidecar Tailscale).
---
<a id="pattern-conflit-path-multi-apps"></a>
## Conflit de path entre apps multiples — anticipation
Quand plusieurs apps cohabitent sur le même domaine :
- **Avant** d'ajouter une app, vérifier que son préfixe ne va pas matcher des routes d'une autre app — voir le cas `/api` dans `knowledge/infra/risques/traefik.md`.
- Préférer des préfixes longs et distinctifs (`/cuisine`, `/portainer`, `/code`) plutôt que génériques (`/app`, `/admin`, `/dashboard`).
- Documenter dans le compose Traefik le mapping path → service pour garder une vue d'ensemble.
- Contexte technique : Traefik v3 / NUC — _Assistant_Cuisine 04-05-2026
+114
View File
@@ -0,0 +1,114 @@
---
title: Infra — Patterns validés : Sidecar Tailscale
domain: infra
bucket: patterns
tags: [tailscale, sidecar, docker, subpath, magicdns, tls, spa]
applies_to: [architecture, implementation]
severity: medium
validated_on: 2026-06-25
source_projects: [apps/stirling-pdf]
---
# Infra — Patterns validés : Sidecar Tailscale
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/infra/patterns/README.md` pour l'index complet.
---
<a id="pattern-sidecar-tailscale-subpath"></a>
## Sidecar Tailscale pour app incompatible avec subpath
Quand une app refuse d'être servie sous un sous-chemin (assets et fetch API hardcodés sur `/`) et qu'on veut la garder derrière Tailscale, on lui attache un **sidecar `tailscale/tailscale`** qui rejoint le tailnet avec son propre hostname et termine le TLS via un cert MagicDNS.
### Quand l'utiliser
- L'app expose une SPA Vite/Next/CRA avec assets hardcodés sur `/`.
- L'app fait des fetch API en chemin absolu (`/api/...`) sans respecter un base path.
- Patcher l'upstream ou injecter un `<base href>` est non trivial / fragile.
- **Critère de décision** : si la première page charge mais que les assets JS retournent 404 derrière Traefik, c'est ce cas (cf. bug upstream Stirling-PDF #5072).
### Architecture
```
tailnet
▼ https://<app>.<tailnet>.ts.net (cert MagicDNS auto)
[container <app>-ts (sidecar tailscale)]
│ network: <app>-internal (bridge dédié)
[container <app>]:<port>
```
- Le sidecar **rejoint le tailnet** comme un nœud à part (compte dans le quota devices).
- Cert TLS délivré automatiquement par `tailscale serve` via `serve.json`.
- L'app reste servie sur `/` côté interne ; **Traefik n'est pas dans la boucle**.
- Réseau Docker dédié, jamais branché sur `traefik-net`.
### Squelette docker-compose
```yaml
services:
myapp:
image: monimage:tag
networks: [app_internal]
# PAS d'exposition de ports sur l'hôte
# PAS de labels traefik.*
tailscale:
image: tailscale/tailscale:v1.98.3
container_name: myapp-ts
# ⚠ PAS de `hostname: myapp` ici (cf. risque collision DNS Docker)
environment:
TS_AUTHKEY: ${MYAPP_TS_AUTHKEY}
TS_HOSTNAME: myapp
TS_STATE_DIR: /var/lib/tailscale
TS_SERVE_CONFIG: /config/serve.json
TS_USERSPACE: "false"
volumes:
- /srv/docker-data/apps/myapp/tailscale/state:/var/lib/tailscale
- /srv/docker-data/apps/myapp/tailscale/serve:/config:ro
- /dev/net/tun:/dev/net/tun
cap_add: [NET_ADMIN, SYS_MODULE]
networks: [app_internal]
labels:
# Auto-discovery homepage : la tile pointe vers le FQDN tailnet, pas vers Traefik
- "homepage.group=Apps"
- "homepage.name=MyApp"
- "homepage.href=https://myapp.<tailnet>.ts.net/"
networks:
app_internal:
name: myapp-internal
driver: bridge
```
`serve.json` :
```json
{
"TCP": { "443": { "HTTPS": true } },
"Web": {
"myapp.<tailnet>.ts.net:443": {
"Handlers": { "/": { "Proxy": "http://myapp:<port-interne>" } }
}
},
"AllowFunnel": { "myapp.<tailnet>.ts.net:443": false }
}
```
> ⚠ Ne pas mettre `hostname: <app>` sur le sidecar si le service voisin s'appelle déjà `<app>` : collision DNS Docker non déterministe. `TS_HOSTNAME` fixe le nom côté tailnet et est décorrélé du DNS Docker interne. Voir `knowledge/infra/risques/docker.md`.
### Init obligatoire
1. Générer une **auth key Tailscale Reusable, non-Ephemeral** dans l'admin console.
2. Stocker le state dans un volume **persistant** (sinon ré-enrôlement à chaque restart et pollution du tailnet).
3. Si le tailnet a "Machine approval" activé : approuver le nœud manuellement après le premier `up`.
4. **Disable key expiry** sur le nœud après enrôlement (sinon il faut refournir une authkey à l'expiration, par défaut 90 j).
### Coût du pattern
- Un nœud tailnet par app (limite gratuite : 100 devices).
- Pas de middlewares Traefik partagés (rewriteBody pour `lang="fr"`, basic auth, etc.) → à accepter, ou patcher l'upstream.
- Double restart si l'app upstream change : le sidecar continue à serve même si l'app est down → 502 propre, pas de cascade.
- Contexte technique : Docker / Tailscale / Spring Boot + Vite — apps/stirling-pdf 27-05-2026
+13
View File
@@ -0,0 +1,13 @@
# Infra — Risques & vigilance — Index
Risques d'infrastructure susceptibles de provoquer des incidents homelab/prod, des pertes de connectivité, ou des bugs non diagnostiquables (Docker, Traefik, Tailscale).
Avant toute proposition infra, identifie le fichier dont le nom et la description matchent le domaine traité, puis lis-le.
---
| Fichier | Domaine | Entrées clés |
|---------|---------|--------------|
| `traefik.md` | Traefik v3, routage, auth, proxy | Incompatibilité API Docker 29 (Traefik 3.6.1+), `PathPrefix(/api)` trop large → regex, Bun `new Response` perd `Location``c.redirect`, basic auth + WebSocket re-prompts WebKit/iOS |
| `tailscale.md` | Certificats Tailscale, MagicDNS | `tailscale cert` ne couvre que le FQDN exact (ni sous-domaines ni wildcard), renouvellement ~3 mois via systemd timer idempotent |
| `docker.md` | DNS Docker, hostname, réseaux partagés | Collision hostname container vs service name (DNS `127.0.0.11` non déterministe), diagnostic `getent hosts`, renvoi post-mortem réseau partagé entre stacks |
+77
View File
@@ -0,0 +1,77 @@
---
title: Infra — Risques & vigilance : Docker
domain: infra
bucket: risques
tags: [docker, dns, hostname, compose, tailscale, networking]
applies_to: [implementation, review, debug, architecture]
severity: high
validated_on: 2026-06-25
source_projects: [apps/stirling-pdf, infra/postgres]
---
# Infra — Risques & vigilance : Docker
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/infra/risques/README.md` pour l'index complet.
---
<a id="risque-docker-collision-dns-hostname-service"></a>
## Collision DNS Docker : hostname container vs service name
### Risques
- Dans un `docker-compose.yml`, déclarer `hostname: <X>` sur un service alors qu'un autre service du même compose s'appelle `<X>` crée **deux entrées A** pour le nom `<X>` dans le DNS embarqué Docker (`127.0.0.11`) :
1. le service name `<X>` (résolu vers l'IP du container du service),
2. le `hostname: <X>` du container voisin.
- Un autre container du même network qui fait `getent hosts <X>` peut tomber sur **l'une ou l'autre selon l'ordre d'enregistrement** — non déterministe entre `docker compose up` successifs (dépend de l'ordre de démarrage).
### Symptômes
- Un service en réseau interne retourne 200 sur son IP directe mais `connection refused` quand on le hit par son service name depuis un voisin.
- **Aucune erreur explicite** dans les logs : juste un `connection refused` qui ressemble à "l'app n'écoute pas".
- Cas typique sidecar Tailscale : `tailscale serve` proxie vers `app:8080` mais le DNS résout `app` vers le sidecar lui-même → 502 côté HTTPS.
### Bonnes pratiques / mitigations
```yaml
# ❌ collision DNS — "app" peut résoudre vers le sidecar
services:
app:
image: monapp # service name DNS = "app"
proxy:
image: nginx
hostname: app # ⚠ collision
# ✅ pas de hostname Docker : le service name reste la seule entrée DNS
services:
app:
image: monapp
proxy:
image: nginx
# hostname Docker non défini → ID container random
# les voisins s'adressent au proxy via le service name "proxy"
```
- **Règle** : ne jamais mettre `hostname: <X>` quand un service du même compose s'appelle `<X>`.
- **Cas sidecar Tailscale** : ne pas mettre `hostname: <app>` pour matcher `TS_HOSTNAME`. `TS_HOSTNAME` est lu par tailscaled et fixe le nom **côté tailnet**, totalement décorrélé du DNS Docker interne. Laisser le hostname Docker par défaut (voir `knowledge/infra/patterns/tailscale.md`).
### Diagnostic
```bash
# Dans le container qui fait l'appel
docker exec <caller> getent hosts <name-resolved>
# Plus d'une ligne pour le même nom → collision DNS Docker
```
- Contexte technique : Docker DNS / Compose / sidecar Tailscale — apps/stirling-pdf 27-05-2026
---
<a id="risque-docker-reseau-partage-stacks"></a>
## Réseau Docker partagé entre stacks — voir post-mortem
Un réseau Docker mutualisé entre plusieurs stacks (ex. Postgres partagé) déclaré `external: true` peut malgré tout être supprimé par un `docker compose down` quand aucun autre projet ne le revendique, avec perte de connectivité en cascade et conteneur recréé sans son mapping de ports.
Détail complet, règles opérationnelles et signal de détection : voir la section **« Réseau Docker partagé entre stacks — créer hors compose »** dans `90_debug_et_postmortem.md`.
- Contexte technique : Docker / réseau partagé / NUC — infra/postgres 22-04-2026
+58
View File
@@ -0,0 +1,58 @@
---
title: Infra — Risques & vigilance : Tailscale
domain: infra
bucket: risques
tags: [tailscale, cert, magicdns, tls, letsencrypt, systemd]
applies_to: [architecture, implementation, debug]
severity: medium
validated_on: 2026-06-25
source_projects: [_Assistant_Cuisine]
---
# Infra — Risques & vigilance : Tailscale
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/infra/risques/README.md` pour l'index complet.
---
<a id="risque-tailscale-cert-fqdn-exact"></a>
## `tailscale cert` ne couvre QUE le FQDN exact du host
### Risques
- `tailscale cert <fqdn>` génère un certificat Let's Encrypt valide **uniquement** pour le FQDN passé, qui doit être le hostname Tailscale du device courant (ex. `nuc.wyvern-snapper.ts.net`). Toute attente de sous-domaines ou de wildcard est fausse.
### Symptômes
- Sous-domaines arbitraires (`cuisine.nuc.wyvern-snapper.ts.net`) : pas de cert auto, et MagicDNS ne route pas non plus.
- Wildcard : non supporté.
- FQDN d'un autre device : non couvert.
### Bonnes pratiques / mitigations
Pour un reverse-proxy multi-apps, deux options :
- **Path-based routing** sous le FQDN unique (`/cuisine`, `/code`, etc.) — voir `knowledge/infra/patterns/reverse-proxy-paths.md`.
- **Vrai domaine** avec challenge DNS-01 (Cloudflare, OVH, etc.) côté Traefik pour obtenir des sous-domaines réels.
**Règle** : `tailscale cert` est OK pour un homelab qui sert tout sous un seul FQDN avec routing par path. Pour des sous-domaines réels, prévoir un domaine perso.
---
<a id="risque-tailscale-cert-renouvellement"></a>
## `tailscale cert` nécessite un renouvellement périodique
### Risques
- Le cert est valide ~3 mois (Let's Encrypt). Sans renouvellement automatisé, expiration silencieuse → TLS cassé sur toutes les apps servies sous ce FQDN.
### Symptômes
- Erreur de certificat expiré côté navigateur après ~90 j sans intervention.
### Bonnes pratiques / mitigations
- **Pattern** : systemd timer hebdomadaire qui appelle `tailscale cert --cert-file ... --key-file ...` sur le FQDN du host. `tailscale cert` est **idempotent** et ne renouvelle qu'à moins de 30 j de la fin — aucun coût à le lancer souvent.
- Le reverse proxy doit watcher le fichier pour reload auto (le file provider de Traefik le fait nativement).
- Contexte technique : Tailscale cert / MagicDNS / systemd — _Assistant_Cuisine 04-05-2026
+109
View File
@@ -0,0 +1,109 @@
---
title: Infra — Risques & vigilance : Traefik
domain: infra
bucket: risques
tags: [traefik, docker, reverse-proxy, bun, websocket, basic-auth, ios]
applies_to: [implementation, review, debug, architecture]
severity: high
validated_on: 2026-06-25
source_projects: [_Assistant_Cuisine]
---
# Infra — Risques & vigilance : Traefik
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/infra/risques/README.md` pour l'index complet.
---
<a id="risque-traefik-v3-docker29-api"></a>
## Pièges Traefik v3 + Docker Engine 29
### Risques
- **Incompatibilité d'API Docker** : Docker 29 a élevé la minimum API version. Traefik < 3.6 utilise un client Docker SDK figé en `1.24` ; le daemon refuse la connexion et aucune route ne fonctionne.
- **`PathPrefix(/api)` trop large** : un routeur `Host(...) && PathPrefix(/api)` vole les routes `/api/*` de toutes les autres apps du même domaine (Homepage `/api/services`, Next.js `/api/...`, n'importe quel SPA) → 401/404 silencieux.
- **Bun `new Response(body, {status: 3xx})` perd le header `Location`** : un proxy HTTP en Bun qui forward un upstream 302/303 retransmet le status mais le client reçoit un 200 vide (ou un status sans `Location`). Le navigateur ne suit pas la redirection.
### Symptômes
- Container Traefik qui tourne mais aucune route active, logs en boucle :
```
ERR Failed to retrieve information of the docker client and server host
error="Error response from daemon: client version 1.24 is too old.
Minimum supported API version is 1.40, please upgrade your client to a newer version"
```
- Une app exposée sur le même domaine que le dashboard Traefik se met à retourner 401/404 sur ses propres `/api/*`.
- Un proxy Bun qui forward une redirection : le client reçoit un 200 vide ou un `Location` nul.
### Bonnes pratiques / mitigations
**Incompatibilité API Docker**
- Utiliser **Traefik 3.6.1+** (client Docker avec négociation auto de version). Les tags `:v3.5` / `:v3.2` ne marchent pas, même avec `DOCKER_API_VERSION=1.44` en env (le client n'honore pas cette variable dans Traefik).
- Pinner explicitement la version (ex. `traefik:v3.6.15`), jamais le tag mouvant `:v3.6`.
- Vérifier la compatibilité Traefik ↔ Docker Engine **avant** un upgrade Docker.
**`PathPrefix(/api)` trop large**
```yaml
# ❌ trop large — capture tous les /api/* du domaine
- "traefik.http.routers.dashboard-api.rule=Host(`example.com`) && PathPrefix(`/api`)"
# ✅ ne capture que les endpoints natifs Traefik
- "traefik.http.routers.dashboard-api.rule=Host(`example.com`) && PathRegexp(`^/api/(overview|version|rawdata|support-dump|entrypoints|http|tcp|udp)(/|$$)`)"
```
Les endpoints natifs Traefik sont fixés : `overview`, `version`, `rawdata`, `support-dump`, `entrypoints`, `http/*`, `tcp/*`, `udp/*`. Sur un domaine partagé entre plusieurs apps : **jamais** de `PathPrefix(/api)` générique, toujours une regex explicite.
**Bun `new Response` perd le `Location`**
```ts
// ❌ perd le Location dans Bun (~1.1.x)
return new Response(upstream.body, {
status: upstream.status, // 303
headers: upstream.headers, // contient Location, ignoré au final
});
// ✅ Hono c.redirect() ou équivalent
if (upstream.status >= 300 && upstream.status < 400) {
const loc = upstream.headers.get("location");
if (loc) return c.redirect(loc, upstream.status as 301 | 302 | 303 | 307 | 308);
}
```
**Règle** : tout proxy HTTP qui forward des redirects upstream doit traiter explicitement la branche 3xx avant la branche "body normal".
- Contexte technique : Traefik v3 / Docker 29 / Bun + Hono — _Assistant_Cuisine 04-05-2026
---
<a id="risque-basic-auth-websocket-webkit-ios"></a>
## Basic auth + WebSocket : re-prompts répétés (surtout WebKit/iOS)
### Risques
- Une app derrière une basic auth (Traefik, nginx) qui utilise des WebSockets (code-server, Jupyter, ttyd, et beaucoup d'apps "live") déclenche **plusieurs prompts d'authentification** à l'ouverture, même quand les credentials viennent d'être saisis.
### Symptômes
- 3 prompts d'authentification successifs à chaque ouverture de l'app, observés notamment sur tablette iOS / iPadOS.
- Persiste sur Chrome iOS et Firefox iOS (tous contraints d'utiliser WebKit sur iOS). Sporadique sur macOS, rare sur desktop Linux/Windows.
### Cause
Safari (et tous les navigateurs iOS, contraints à WebKit) ne **propage pas systématiquement les credentials basic auth aux requêtes WebSocket** d'une page déjà authentifiée. Chaque WS qui échoue redéclenche un prompt.
### Bonnes pratiques / mitigations
Remplacer la basic auth par une auth à **cookie de session** :
- Soit l'auth native de l'app si elle existe (code-server `HASHED_PASSWORD`, JupyterHub login form, etc.).
- Soit un système central type Authelia / Authentik / traefik-forward-auth qui pose un cookie partagé entre apps.
Le cookie passe automatiquement avec les requêtes WebSocket (même origine) → plus de re-prompt.
**Règle** : pour toute app qui utilise des WebSockets ET qui doit être protégée, ne **pas** utiliser la basic auth. Toujours cookie-based. La basic auth reste OK pour des UI sans WS (dashboards en pull, API REST simples).
- Contexte technique : Traefik / basic auth / WebKit iOS — _Assistant_Cuisine 04-05-2026