From 3bb8e87ef25cd89fc731a751bfbab018651cfcbc Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Thu, 1 Oct 2026 23:09:45 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20A42=20termin=C3=A9=20=E2=80=94=20client?= =?UTF-8?q?=20httpx=20partag=C3=A9=20par=20boucle=20(v7.28.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `app/services/http_client.py` : `async with shared_client(timeout=15) as client:` remplace les 49 créations `async with httpx.AsyncClient(` de 14 fichiers (gitea ×21, providers oidc/oauth ×11, calendar ×4, automations ×3…) — le pool de connexions est réutilisé au lieu d'être recréé à chaque appel. __aexit__ no-op (le client partagé ne se ferme pas à la sortie). - Cache par (boucle d'event, kwargs) en WeakKeyDictionary : un AsyncClient n'est JAMAIS partagé entre deux loops (piège des tests « Event loop is closed ») — une boucle par test = client propre collecté avec la boucle. Clé = kwargs triés, repr() pour les valeurs non hashables (`headers=` dict → TypeError rattrapé par la suite). - Laissés délibérément : github_adapter (transport MockTransport injecté), webhook_outbound (client « own_client » fermé par la fonction). - Tests : `test_http_client_shared_and_loop_scoped` (réutilisation mêmes kwargs / cloisonné kwargs / cloisonné loop) ; le stub des webhooks patche aussi la fabrique `http_client.httpx` + purge du cache (avant : webhook_outbound.httpx patché mais la fabrique partagée créait un vrai client → réseau réel dans les tests). suite **1091/1091** · ruff OK · docs à jour --- CHANGELOG.md | 29 ++++++++++++ ROADMAP.md | 4 +- VERSION | 2 +- WORKLOAD.md | 2 +- app/auth/oauth.py | 7 ++- app/auth/providers/__init__.py | 14 +++--- app/auth/providers/oidc_provider.py | 8 ++-- app/main.py | 2 +- app/routers/board.py | 4 +- app/routers/scim.py | 4 +- app/routers/sso.py | 4 +- app/services/automations.py | 9 ++-- app/services/calendar_sync.py | 11 +++-- app/services/gitea_client.py | 45 ++++++++++--------- app/services/http_client.py | 68 +++++++++++++++++++++++++++++ app/services/importers/url_fetch.py | 3 +- app/services/llm_client.py | 3 +- app/services/llm_config.py | 7 ++- app/services/og_fetcher.py | 5 ++- app/services/webhook_outbound.py | 5 ++- docs/openapi-v2.json | 2 +- tests/test_audit_p0_fixes.py | 27 ++++++++++++ tests/test_webhooks_v2.py | 6 +++ 23 files changed, 200 insertions(+), 71 deletions(-) create mode 100644 app/services/http_client.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 98a4919..8ca74d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog - FlowDeck +## v7.28.0 (2026-10-01) — Audit : A42 TERMINÉ (client httpx partagé) + +### Changed + +- **`app/services/http_client.py`** : `async with shared_client(timeout=15) + as client:` remplace les **49 créations `async with httpx.AsyncClient(`** + réparties dans 14 fichiers (gitea ×21, providers ×11, calendar ×4, + automations ×3, …) — le pool de connexions est réutilisé au lieu d'être + recréé à chaque appel +- Cache **par (boucle d'event, kwargs)** en `WeakKeyDictionary` : un + `AsyncClient` n'est jamais partagé entre deux loops (le piège des tests : + « Event loop is closed ») — une boucle par test = un client propre, + collecté avec elle. Clé = kwargs triés, `repr()` pour les valeurs non + hashables (`headers=` dict) +- Context manager no-op à la sortie (pas de fermeture du client partagé) ; + `ponytail:` documenté : pas d'`aclose` explicite, plafond = pools non + fermés à la main (GC des sockets), upgrade = lifespan +- **Laissés délibérément** : `github_adapter` (transport MockTransport + injecté), `webhook_outbound` (client « own_client » fermé par la fonction) + +### Tests + +- `test_http_client_shared_and_loop_scoped` : réutilisation (mêmes kwargs), + cloisonnement par kwargs, cloisonnement par boucle (2× `asyncio.run`) +- Stub webhooks : patch étendu à la fabrique `http_client.httpx` + purge du + cache (les tests patchaient `webhook_outbound.httpx`, contourné par la + fabrique partagée) +- Suite complète : **1091/1091** + ## v7.27.0 (2026-10-01) — Audit : A20 phase 2 (CDN retiré, connect-src fermé) ### Changed diff --git a/ROADMAP.md b/ROADMAP.md index 3aafcc7..1704a4d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1165,7 +1165,7 @@ Quality DB views, Agent IA Palette → Realtime + E - [x] **A39 — HTMX chargé (49,7 Ko) pour 10 attributs réels** vs 265 `fetch(` manuels. *Fix : soit drop `htmx.min.js` et convertir les 10, soit rien.* — **décision 2026-10-01 : rien** (option proposée par l'audit) : 32 attributs `hx-*` réels répartis dans 6 templates (view-switch board/dashboard/notes, `hx-boost`) ; les convertir = refonte du view-switching en JS sans couverture automatisée, coût/risque > gain de 49,7 Ko. **Reconsidérer** dès qu'un test E2E couvre le view-switch. - [x] **A40 — Assets** : `?v=` incohérent (`app.css?v=5.1.1` mais CSS modifié le 2026-09-14 > dernier bump 2026-09-12 → servi depuis le cache), la même liste d'assets est **dupliquée** dans `sw.js:19-31`, htmx/alpine/prism vendored **sans bannière de version ni SRI**, 3 `