Skip to content

fix(api-client): share a long-lived httpx client, configure timeouts, and single-flight JWKS / OIDC refetch#86

Open
jackton1 wants to merge 2 commits intoauth0:mainfrom
jackton1:fix/shared-httpx-and-single-flight
Open

fix(api-client): share a long-lived httpx client, configure timeouts, and single-flight JWKS / OIDC refetch#86
jackton1 wants to merge 2 commits intoauth0:mainfrom
jackton1:fix/shared-httpx-and-single-flight

Conversation

@jackton1
Copy link
Copy Markdown

@jackton1 jackton1 commented Apr 19, 2026

Changes

ApiClient._fetch_jwks and _fetch_oidc_metadata build a fresh httpx.AsyncClient() on every cache miss with no explicit timeout and no single-flight protection. Under concurrent load, TTL expiry turns into a thundering herd of ConnectTimeout errors that surface as UnknownAuth0Exception / "Unknown auth error". Still reproducible on 1.0.0b8.

This PR:

  • utils.py — adds a lazily-initialized module-level httpx.AsyncClient with explicit timeouts (connect=5s, read=10s, write=5s, pool=5s), keep-alive pool limits, and AsyncHTTPTransport(retries=2). fetch_jwks and fetch_oidc_metadata reuse it when no custom_fetch is supplied. Adds idempotent aclose_default_httpx_client().
  • api_client.pyApiClient.__init__ allocates per-key asyncio.Lock maps (_discovery_locks, _jwks_locks). _discover and _fetch_jwks now do a fast cache check, take the per-key lock, re-check, then fetch — so N concurrent misses for the same key produce exactly one upstream call. Adds idempotent ApiClient.aclose().
  • tests/test_concurrent_fetch.py — new tests for the above.

Not changed: public API surface, custom_fetch behaviour, cache implementation / TTL semantics. No new dependencies.

References

Testing

tests/test_concurrent_fetch.py (6 tests, pytest-asyncio + pytest-httpx) covers:

  • 50 concurrent JWKS misses for the same URI → exactly 1 upstream fetch.
  • 50 concurrent OIDC discovery misses → exactly 1 upstream fetch.
  • Misses for different JWKS URIs are not serialized behind a global lock.
  • Default httpx client is a singleton with explicit (non-default) timeouts.
  • aclose() is idempotent and the client can be re-created afterward.
217 passed   # full existing suite, no regressions
6 passed     # new concurrent-fetch tests

Reproduce:

poetry install
poetry run pytest tests/test_concurrent_fetch.py -v
poetry run pytest

Integration tests against a live tenant were not added — the existing project mocks the network layer, and the ConnectTimeout storm under TTL expiry is exercised deterministically by pytest-httpx.

  • This change adds unit test coverage
  • This change adds integration test coverage
  • This change has been tested on the latest version of the platform/language or why not

Checklist

@jackton1 jackton1 requested a review from a team as a code owner April 19, 2026 05:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant