Skip to content

HttpRequestor: do not reject credentials on HTTP 400 - #2088

Open
tyrielv wants to merge 1 commit into
microsoft:masterfrom
tyrielv:user/tyvella/no-reject-credentials-on-400
Open

HttpRequestor: do not reject credentials on HTTP 400#2088
tyrielv wants to merge 1 commit into
microsoft:masterfrom
tyrielv:user/tyvella/no-reject-credentials-on-400

Conversation

@tyrielv

@tyrielv tyrielv commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Summary

GVFS erased a valid credential when an object-download response was HTTP 400, which triggered a storm of Git Credential Manager popups.

A 400 is a request or formatting problem, not an authentication failure. An expired or invalid credential always returns 401 (Unauthorized) or 302 (the Azure DevOps sign-in redirect), never 400. Treating 400 as an auth failure erased good credentials and showed the misleading "Your PAT may be expired" message.

Change

  • In HttpRequestor.SendRequest, remove BadRequest (400) from the credential-rejection branch. Only 401 and 302 now call RejectCredentials.
  • A 400 now flows through the generic, non-auth error path — same retry / circuit-breaker behavior as before, and 400 remains non-retryable (ShouldRetry never included it).
  • Extract the decision into a testable ShouldRejectCredentials(HttpStatusCode) helper.

Why 400 is never an auth failure

Verified against the real Azure DevOps and GVFS cache-server endpoints:

  • A bad or expired credential returns 401 (both dev.azure.com and the cache server); without X-TFS-FedAuthRedirect: Suppress, dev.azure.com returns a 302 to the sign-in page — so 302 is genuinely auth-related.
  • A valid but nonexistent object returns 404.
  • A malformed request (a corrupt placeholder SHA produced a URL the Azure Application Gateway rejected) returns 400 with an empty X-Cache-Name and a generic gateway body — not an auth problem.
  • The only auth-adjacent 400 is a completely missing Basic auth header ("A valid Basic Authorization header is required."), i.e. no credential was sent. That is a GVFS-side bug, not an expired credential, so erasing the stored credential is still the wrong response.

Tests

Added GVFS.UnitTests/Http/HttpRequestorTests.cs:

  • 400 does not reject credentials.
  • 401 and 302 do reject credentials.
  • 404 / 500 / 408 do not reject credentials.

New tests pass (4/4); full unit-test suite passes.

GVFS erased a valid credential when an object-download response was HTTP 400,
which triggered a storm of Git Credential Manager popups.

A 400 is a request or formatting problem, not an authentication failure. An
expired or invalid credential always returns 401 (Unauthorized) or 302 (the
Azure DevOps sign-in redirect), never 400. Treating 400 as an auth failure
erased good credentials and produced the misleading "Your PAT may be expired"
message.

Remove BadRequest (400) from the credential-rejection branch in SendRequest.
Only 401 and 302 now reject credentials; 400 flows through the generic,
non-auth error path (unchanged retry / circuit-breaker behavior, and 400
remains non-retryable). Extract the decision into ShouldRejectCredentials so it
is unit tested: 400 does not reject credentials, while 401 and 302 still do.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
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