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
- Run 'az login' (or ensure the workload identity/managed identity is valid) and confirm the identity has ACR pull access on the registry.
- Check the registry endpoint/host is correct for the ACR instance.
- Retry on transient status codes (429, 5xx) with backoff; the exception exposes the StatusCode for this.
- 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
- Ensure a valid az login / managed identity before docker/ACR operations.
- Grant the identity AcrPull on the registry.
- Implement backoff retry for 429/5xx.
- Log the exception's Body snippet for the service error detail.
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
- Response missing refresh_token.
- ADC request ' ' failed with HTTP ( ).
- ADC request ' ' returned an empty response.
- Cannot create the cross-scope ACR pull identity
- Existing Azure sandbox group
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)