Repository navigation
fix: replace the generated client on refresh instead of mutating its token - #36
Merged
Merged
Conversation
…token A refreshed token never reached the wire. `AuthenticatedClient` writes the Authorization header into an `httpx.Client` the first time it is used and caches that client; `_authenticate()` and `_ensure_client()` then assigned `client.token`, which updates the attribute and nothing else. Every request after the first token expiry carried the expired token. The symptom is severe and silent: the API answers 403 with an explicit-deny message, a caller's retry mints another token and is rejected the same way, and only a process restart recovers. A downstream live runtime lost sixteen hours of fixture discovery to this — 1442 rejected calls in a day — while a one-off script with the same credentials worked. Token changes now discard the client so the next call rebuilds it, and the superseded transport is closed rather than leaked. `close()` releases it too, which it previously never did. The new tests assert the invariant without touching HTTP: the constructor performs no network I/O, and a pre-set expiry keeps `_ensure_client()` from authenticating, so a token swap can be driven directly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mad4ms
added a commit
that referenced
this pull request
Sep 12, 2026
Patch release carrying the token-refresh fix from #36. Consumers on 0.3.0 and 0.2.0 lose every DataCore call with a 403 once their first token expires, and recover only by restarting the process, so this wants to reach PyPI promptly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
A refreshed token never reaches the wire.
AuthenticatedClientwrites the Authorization header into anhttpx.Clientthe first time it is used, and caches that client:_authenticate()and_ensure_client()then assignedclient.token, which updates the attribute and nothing else. Every request after the first token expiry carried the expired token.The failure mode is severe and quiet: the API answers
403with an explicit-deny message, a caller's retry mints another token and is rejected identically, and only a process restart recovers.This was found the hard way. A downstream live runtime lost sixteen hours of fixture discovery to it — 1442 rejected calls in one day — while a one-off script using the very same credentials on the same host worked fine. The journal shows the mechanism exactly:
The change
Token changes now discard the generated client so the next call rebuilds it with the new header, and the superseded transport is closed instead of leaked.
close()releases it too, which it previously never did — onlysessionwas closed._ensure_client()loses itselse: self.client.token = tokenbranch: with every refresh going through_discard_client(), a surviving client always carries the current token.Tests
test/test_token_refresh.pyasserts the invariant without touching HTTP, per the repo rule against mocking the transport: the constructor performs no network I/O, and a pre-set expiry keeps_ensure_client()from authenticating, so a token swap can be driven directly. The first test fails onmainwith anAttributeErrorwhere it expects a rebuilt client.pytest: 12 passed, 7 skipped (the live-API tests, no credentials present)ruff check,ruff format --check,mypy --strict: cleanNote for the release
Downstream pins
>=0.2.0and currently resolves 0.2.0, which carries the same bug. A release would let consumers drop their local workarounds.🤖 Generated with Claude Code