fix(cors): /api/search ne doit plus empiler les en-têtes CORS (défaut 3b) #41

Merged
bruno merged 2 commits from fix-cors-search-doublons into main 2026-10-28 18:22:10 -04:00
Owner

Cause racine prouvée (défaut 3b)

Onglet Flowdeck de la recherche console agent-dev : « recherche indisponible pour l'instant (injoignable (CORS ou réseau)) » — c'est la branche .catch() du fetch console, donc le navigateur a rejeté la réponse avant toute lecture (pas un HTTP 401/404, pas un DNS : l'instance répond).

Maillons vérifiés un par un :

  1. Origine appelée : CORRECTE. GET /api/v1/config-console d'agent-dev retourne flowdeck = https://flowdeck-dev.dracodev.net (dérivation -dev par hôte de requête, app/config.py urls_apps_pour_hote) — pas la prod. La CSP console (même fonction) couvre donc flowdeck-dev en connect-src.
  2. Allowlist CORS de /api/search : CORRECTE sur le papier. app/middleware/recherche_cors.py : SEARCH_PATH = /api/search (exactement la route appelée), AGENT_ORIGINS = agent prod ET dev, preflight OPTIONS propre (204, en-têtes uniques, vérifié en déployé).
  3. Le maillon fautif : L'EMPILEMENT. Mesure en déployé sur flowdeck-dev (GET /api/search, Origin agent-dev, 401 attendu sans session) : Access-Control-Allow-Credentials apparaît DEUX fois, Allow-Origin une fois. Explication : le CORSMiddleware global pose déjà Allow-Credentials: true sur TOUTES les réponses (comportement Starlette avec allow_credentials=True — vérifié : même pour une origine hors allowlist et hors /api/search), et l'ancien middleware de recherche AJOUTAIT ses en-têtes par-dessus. Le navigateur combine les doublons (« true, true » ≠ « true ») et rejette le fetch. Depuis une origine sœur déjà dans la allowlist globale (og-dev), c'est pire : Allow-Origin x2, Allow-Credentials x2, Vary x2 (mesuré). TikTak (remplacement via response.headers[key] = value) et Kanbante (pas de CORSMiddleware global) répondent avec des en-têtes uniques — d'où l'échec Flowdeck seul.
  4. Authentification : hors de cause pour ce symptôme (un échec SSO donnerait « HTTP 401 », pas le .catch()). Le chemin d'endpoint et les credentials sont inchangés.

Correctif (un seul fichier)

  • app/middleware/recherche_cors.py, send_with_cors : REMPLACER les en-têtes CORS existants (Allow-Origin, Allow-Credentials, Vary filtrés puis ré-ajoutés) au lieu de les empiler — même patron que app/recherche_cors.py de la console. Origine non autorisée : traverse sans modification (comportement antérieur préservé). Aucun changement de allowlist, de route, de CSP, de dérivation : la prod (agent → hôtes prod) et TikTak/Kanbante sont inchangés.

Tests (chiffres réels, exécutés ici)

  • Harnais ASGI sur le vrai middleware : ANCIEN code reproduit le déployé (agent-dev : ACAO 1 / ACAC 2 / Vary 1 ; og-dev : 2 / 2 / 2 ; refusé : 0 / 1 / 0) ; NOUVEAU code donne partout (1 / 1 / 1) et (0 / 1 / 0) pour le refusé, statut 401 traversant inchangé.
  • tests/test_recherche_cors_doublons.py (pytest, CI) : 4 tests — agent-dev un seul jeu, sœur sans doublon, origine refusée inchangée, preflight unique.
  • py_compile OK. Non vérifiable ici : pytest/CI complets (pas de fastapi/pytest dans la sandbox) et le test navigateur déployé — à valider par Bruno après redéploiement (voir ci-dessous).
  • Mesures déployées annexes (avant correctif) : flowdeck-dev /api/health = 7.84.5 (code #40 bien déployé en -dev) ; /api/search sans session = 401 JSON (route vivante) ; TikTak/Kanbante/ObsiGate -dev = un seul jeu d'en-têtes chacun.

Contraintes respectées

Branche + PR uniquement, aucun merge, aucune publication, aucun bump de version, aucun état d'issue touché, aucune autre PR touchée. Dépôt agent-commun non modifié par cette PR (sa PR est séparée).

À valider en déployé par Bruno (après redéploiement flowdeck-dev)

  • Onglet Flowdeck de la recherche console agent-dev : résultats réels, plus « injoignable (CORS ou réseau) ».
  • Onglets TikTak/Kanbante : non régressés ; onglet ObsiGate : couvert par la PR agent-commun séparée.
  • Prod inchangée : agent prod → flowdeck prod, mêmes en-têtes uniques.
## Cause racine prouvée (défaut 3b) Onglet Flowdeck de la recherche console agent-dev : « recherche indisponible pour l'instant (injoignable (CORS ou réseau)) » — c'est la branche .catch() du fetch console, donc le navigateur a rejeté la réponse avant toute lecture (pas un HTTP 401/404, pas un DNS : l'instance répond). Maillons vérifiés un par un : 1. Origine appelée : CORRECTE. GET /api/v1/config-console d'agent-dev retourne flowdeck = https://flowdeck-dev.dracodev.net (dérivation -dev par hôte de requête, app/config.py urls_apps_pour_hote) — pas la prod. La CSP console (même fonction) couvre donc flowdeck-dev en connect-src. 2. Allowlist CORS de /api/search : CORRECTE sur le papier. app/middleware/recherche_cors.py : SEARCH_PATH = /api/search (exactement la route appelée), AGENT_ORIGINS = agent prod ET dev, preflight OPTIONS propre (204, en-têtes uniques, vérifié en déployé). 3. Le maillon fautif : L'EMPILEMENT. Mesure en déployé sur flowdeck-dev (GET /api/search, Origin agent-dev, 401 attendu sans session) : Access-Control-Allow-Credentials apparaît DEUX fois, Allow-Origin une fois. Explication : le CORSMiddleware global pose déjà Allow-Credentials: true sur TOUTES les réponses (comportement Starlette avec allow_credentials=True — vérifié : même pour une origine hors allowlist et hors /api/search), et l'ancien middleware de recherche AJOUTAIT ses en-têtes par-dessus. Le navigateur combine les doublons (« true, true » ≠ « true ») et rejette le fetch. Depuis une origine sœur déjà dans la allowlist globale (og-dev), c'est pire : Allow-Origin x2, Allow-Credentials x2, Vary x2 (mesuré). TikTak (remplacement via response.headers[key] = value) et Kanbante (pas de CORSMiddleware global) répondent avec des en-têtes uniques — d'où l'échec Flowdeck seul. 4. Authentification : hors de cause pour ce symptôme (un échec SSO donnerait « HTTP 401 », pas le .catch()). Le chemin d'endpoint et les credentials sont inchangés. ## Correctif (un seul fichier) - app/middleware/recherche_cors.py, send_with_cors : REMPLACER les en-têtes CORS existants (Allow-Origin, Allow-Credentials, Vary filtrés puis ré-ajoutés) au lieu de les empiler — même patron que app/recherche_cors.py de la console. Origine non autorisée : traverse sans modification (comportement antérieur préservé). Aucun changement de allowlist, de route, de CSP, de dérivation : la prod (agent → hôtes prod) et TikTak/Kanbante sont inchangés. ## Tests (chiffres réels, exécutés ici) - Harnais ASGI sur le vrai middleware : ANCIEN code reproduit le déployé (agent-dev : ACAO 1 / ACAC 2 / Vary 1 ; og-dev : 2 / 2 / 2 ; refusé : 0 / 1 / 0) ; NOUVEAU code donne partout (1 / 1 / 1) et (0 / 1 / 0) pour le refusé, statut 401 traversant inchangé. - tests/test_recherche_cors_doublons.py (pytest, CI) : 4 tests — agent-dev un seul jeu, sœur sans doublon, origine refusée inchangée, preflight unique. - py_compile OK. Non vérifiable ici : pytest/CI complets (pas de fastapi/pytest dans la sandbox) et le test navigateur déployé — à valider par Bruno après redéploiement (voir ci-dessous). - Mesures déployées annexes (avant correctif) : flowdeck-dev /api/health = 7.84.5 (code #40 bien déployé en -dev) ; /api/search sans session = 401 JSON (route vivante) ; TikTak/Kanbante/ObsiGate -dev = un seul jeu d'en-têtes chacun. ## Contraintes respectées Branche + PR uniquement, aucun merge, aucune publication, aucun bump de version, aucun état d'issue touché, aucune autre PR touchée. Dépôt agent-commun non modifié par cette PR (sa PR est séparée). ## À valider en déployé par Bruno (après redéploiement flowdeck-dev) - Onglet Flowdeck de la recherche console agent-dev : résultats réels, plus « injoignable (CORS ou réseau) ». - Onglets TikTak/Kanbante : non régressés ; onglet ObsiGate : couvert par la PR agent-commun séparée. - Prod inchangée : agent prod → flowdeck prod, mêmes en-têtes uniques.
bruno added 2 commits 2026-10-28 16:50:19 -04:00
fix(cors): /api/search remplace les en-tetes CORS au lieu de les empiler (defaut 3b)
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (push) Failing after 2m9s
4490a2ed70
test(cors): /api/search un seul jeu d'en-tetes CORS, origine refusee inchangee (defaut 3b)
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (pull_request) Failing after 1m49s
FlowDeck CI / test (pull_request) Failing after 32m50s
FlowDeck CI / docker (pull_request) Skipped
34253c7b3f
bruno merged commit fb4ce38403 into main 2026-10-28 18:22:10 -04:00
Sign in to join this conversation.