fix(azure): 401 fallback to anonymous for read-only requests#166
Merged
Conversation
PR #165 enabled anonymous access to public Azure DevOps projects when AZURE_DEVOPS_TOKEN is unset, but a stale or expired PAT in the env still defeated that path: the client attached BasicAuth and Azure returned 401 ("Personal Access Token has expired") before the anonymous code path could be reached. This patch makes read-only access robust to stale credentials. When a GET request returns 401 with auth attached, the client transparently retries once without the auth header. Public projects succeed on the retry; private projects still return 401 to the caller. Behavior: - GET with auth -> 200: returned directly (no retry) - GET with auth -> 401: retried without auth, second response returned - GET without auth -> 401: returned directly (no retry — already anon) - POST/PATCH -> 401: propagated unchanged (writes require valid auth) Tests cover all four paths in TestAzureClient401Fallback. Lint/format/typecheck: green. Pytest: deferred to CI.
|
Claude finished @MementoRC's task —— View job PR Review Complete ✅Todo List
Review FeedbackThis is a well-crafted fix for a real-world issue with expired Azure DevOps PATs. The implementation is clean, focused, and includes excellent test coverage. ✅ StrengthsImplementation Quality:
Logic & Design:
Test Coverage:
🔍 Technical ReviewSecurity Considerations:
Performance Considerations:
Error Handling:
🎯 Specific Code Review
if response.status == 401:
await response.release() # ✅ Proper cleanup
logger.warning("Azure 401 with auth — retrying anonymously for public project access")
response = await self.session.get(url, headers=headers, **kwargs) # ✅ No auth kwargTest Quality (
🏆 Overall AssessmentThis PR demonstrates excellent software engineering practices:
The implementation elegantly handles the edge case where expired tokens prevent fallback to anonymous access, which is exactly what was needed after PR #165. Recommendation: ✅ Approve - This is ready to merge. |
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.
Summary
PR #165 enabled anonymous access to public Azure DevOps projects when
AZURE_DEVOPS_TOKENis unset. However, live testing against build 1517588 exposed a remaining gap: when a stale or expired PAT is present in the environment, the client still attachesBasicAuthand Azure returns401 ("Personal Access Token has expired")before the anonymous code path can engage.This patch makes GET requests robust to stale credentials by transparently retrying without auth on 401.
Gap Left by PR #165
The anonymous path in #165 is gated on
token is None. IfAZURE_DEVOPS_TOKENis set (even to an expired value),_auth()returns aBasicAuthobject and the request is sent with credentials. Azure then rejects it at the auth layer — the public-project anonymous path is never tried.Build 1517588 returned 401 from the merged code precisely because an expired PAT was still set in the env.
Fix
In
AzureClient.get(), after a first-attempt 401 with auth attached:POST and PATCH are unchanged — writes require valid auth.
Behavior Table
Test Coverage
New class
TestAzureClient401Fallbackintests/unit/azure/test_azure_client.pycovers all four paths:test_get_retries_without_auth_on_401_when_token_present— verifies 2 calls, release awaited, second call has noauthkwarg, returns 200 responsetest_get_does_not_retry_on_401_when_anonymous— verifies only 1 call when token is Nonetest_get_returns_first_response_on_200— verifies no retry when first response is 200test_post_does_not_retry_on_401— verifies POST 401 propagated, GET never calledFiles Changed
src/mcp_server_git/azure/client.py— GET retry logic (6 lines added)tests/unit/azure/test_azure_client.py—TestAzureClient401Fallbackclass (4 tests)