fix: A42 terminé — client httpx partagé par boucle (v7.28.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s

- `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
This commit is contained in:
2026-10-01 23:09:45 -04:00
parent 069c438aae
commit 3bb8e87ef2
23 changed files with 200 additions and 71 deletions
+2 -2
View File
@@ -16,6 +16,7 @@ from app.routers.dashboard import _get_app_version
from app.routers.sidebar_config import get_sidebar_config_sync
from app.services.automations import fire_event, run_event_sync
from app.services.gitea_client import gitea
from app.services.http_client import shared_client
from app.services.permission_manager import PermissionManager
from app.services.publish import fire_published, fire_unpublished, publish, unpublish
@@ -1880,8 +1881,7 @@ async def _unfurl_repo(forge: str, owner: str, repo: str):
if token:
info = await GitHubAdapter(access_token=token).get_repo_info(owner, repo)
else:
import httpx
async with httpx.AsyncClient(timeout=10) as client:
async with shared_client(timeout=10) as client:
r = await client.get(
f"https://api.github.com/repos/{owner}/{repo}",
headers={"Accept": "application/vnd.github+json"},
+2 -2
View File
@@ -18,6 +18,7 @@ from fastapi.responses import JSONResponse
from app.auth.session import SessionManager
from app.db import get_conn
from app.services.api_v2_helpers import audit_log
from app.services.http_client import shared_client
router = APIRouter(tags=["scim"])
SCIM_SCHEMAS = ["urn:ietf:params:scim:schemas:core:2.0:User"]
@@ -246,7 +247,6 @@ def create_domain(request: Request, body: dict = Body(default={})):
@router.post("/api/v2/domain-claims/{domain_id}/verify")
async def verify_domain(domain_id: int, request: Request):
admin = _admin_session(request)
import httpx
with get_conn() as conn:
row = conn.execute("SELECT * FROM domain_claims WHERE id=?", (domain_id,)).fetchone()
if not row:
@@ -254,7 +254,7 @@ async def verify_domain(domain_id: int, request: Request):
claim = dict(row)
url = f"https://{claim['domain']}/.well-known/flowdeck-verify.txt"
try:
async with httpx.AsyncClient(timeout=10, follow_redirects=True) as client:
async with shared_client(timeout=10, follow_redirects=True) as client:
resp = await client.get(url)
ok = resp.status_code == 200 and claim["txt_token"] in (resp.text or "")
except Exception: # noqa: BLE001 — unreachable domain = not verified
+2 -2
View File
@@ -24,6 +24,7 @@ from app.auth.providers import oidc_provider, saml_provider
from app.auth.session import SessionManager
from app.services import sso_provisioning as sso
from app.services.api_v2_helpers import has_scope, resolve_bearer_token
from app.services.http_client import shared_client
logger = logging.getLogger(__name__)
router = APIRouter(tags=["sso"])
@@ -436,10 +437,9 @@ async def _fetch_jwks(doc: dict) -> dict:
url = doc.get("jwks_uri")
if not url:
raise oidc_provider.OIDCError("Discovery document has no jwks_uri")
import httpx
try:
async with httpx.AsyncClient(timeout=15) as client:
async with shared_client(timeout=15) as client:
r = await client.get(url)
r.raise_for_status()
data = r.json()