microsoft/aspire · error · HttpRequestException

POST /oauth2/exchange failed

Error message

POST /oauth2/exchange failed {(int)response.StatusCode} {response.ReasonPhrase}. Body: {truncatedBody}

What it means

AcrLoginService exchanges an AAD token for an ACR refresh token by POSTing to /oauth2/exchange. When the registry service returns a non-success status, the response body (truncated to 1000 chars) is wrapped in an HttpRequestException carrying the status code.

Solutions

  1. Run 'az login' (or ensure the workload identity/managed identity is valid) and confirm the identity has ACR pull access on the registry.
  2. Check the registry endpoint/host is correct for the ACR instance.
  3. Retry on transient status codes (429, 5xx) with backoff; the exception exposes the StatusCode for this.
  4. Inspect the included response Body for the service-side error detail.

Example fix

try
{
    var refreshToken = await acrLoginService.RefreshTokenAsync(...);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
{
    // re-authenticate (az login / acquire new AAD token) and retry
}
Defensive patterns

Strategy: retry

Validate before calling

// Before calling: ensure credentials exist
if (string.IsNullOrEmpty(aadToken)) throw new InvalidOperationException("Acquire an AAD token via az login or DefaultAzureCredential first.");

Try / catch

try { return await RefreshTokenAsync(ct); }
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
{ /* re-authenticate */ throw; }
catch (HttpRequestException ex) when ((int?)ex.StatusCode is 429 or >= 500) { await Task.Delay(TimeSpan.FromSeconds(2), ct); /* retry */ throw; }

Prevention

When it happens

Trigger: Calling refreshToken -> ExchangeAadTokenForAcrRefreshTokenAsync when Azure Container Registry rejects the token exchange: expired/insufficient-scope AAD token, wrong registry endpoint, or registry-side auth errors (401/403/429/5xx).

Common situations: Not logged in with az login or the identity lacks ACR pull permissions; wrong registry name in the endpoint URL; transient AAD/ACR outages; using managed identity in an environment where it isn't available.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of microsoft/aspire@25830f84bd (2026-09-16). Data as JSON: /api/errors/3fec4d7856b6fcfa. Report an issue: GitHub.

Appendix: source

Thrown at src/Aspire.Hosting.Azure/AcrLoginService.cs:175

        var formData = new Dictionary<string, string>
        {
            ["grant_type"] = "access_token",
            ["service"] = registryEndpoint,
            ["tenant"] = tenantId,
            ["access_token"] = aadAccessToken
        };

        using var content = new FormUrlEncodedContent(formData);
        var response = await httpClient.PostAsync(exchangeUrl, content, cancellationToken).ConfigureAwait(false);

        // Read response body as string once
        var responseBody = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);

        if (!response.IsSuccessStatusCode)
        {
            var truncatedBody = responseBody.Length <= 1000 ? responseBody : responseBody[..1000] + "…";
            throw new HttpRequestException(
                $"POST /oauth2/exchange failed {(int)response.StatusCode} {response.ReasonPhrase}. Body: {truncatedBody}",
                null,
                response.StatusCode);
        }

        // Deserialize from the string we already read
        var tokenResponse = JsonSerializer.Deserialize<AcrRefreshTokenResponse>(responseBody, s_jsonOptions);

        if (string.IsNullOrEmpty(tokenResponse?.RefreshToken))
        {
            throw new InvalidOperationException($"Response missing refresh_token.");
        }

        return tokenResponse.RefreshToken;
    }
}

View on GitHub (pinned to 25830f84bd)