// Copyright (c) Microsoft. All rights reserved. using System; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using Azure.Core; namespace Microsoft.Agents.AI.Foundry.Hosting; /// /// An that: /// /// Acquires a fresh Azure bearer token (scope: https://cognitiveservices.azure.com/.default) per request. /// Injects the Foundry-Features header from FOUNDRY_AGENT_TOOLSET_FEATURES when non-empty. /// Retries on HTTP 429, 500, 502, and 503 with exponential back-off (max 3 attempts, per spec §7). /// /// internal sealed class FoundryToolboxBearerTokenHandler : DelegatingHandler { private const int MaxRetries = 3; private static readonly TokenRequestContext s_tokenContext = new(["https://cognitiveservices.azure.com/.default"]); private readonly TokenCredential _credential; private readonly string? _featuresHeaderValue; internal FoundryToolboxBearerTokenHandler(TokenCredential credential, string? featuresHeaderValue) { this._credential = credential; this._featuresHeaderValue = featuresHeaderValue; } protected override async Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { var token = await this._credential .GetTokenAsync(s_tokenContext, cancellationToken) .ConfigureAwait(false); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token); if (!string.IsNullOrEmpty(this._featuresHeaderValue)) { request.Headers.TryAddWithoutValidation("Foundry-Features", this._featuresHeaderValue); } // MaxRetries is the total number of attempts (not additional retries after the first). for (int attempt = 0; attempt < MaxRetries; attempt++) { // Clone the request for retries (the original request cannot be sent twice) HttpRequestMessage requestToSend = attempt == 0 ? request : await CloneRequestAsync(request, cancellationToken).ConfigureAwait(false); var response = await base.SendAsync(requestToSend, cancellationToken).ConfigureAwait(false); if (response.StatusCode is not (HttpStatusCode.TooManyRequests or HttpStatusCode.InternalServerError or HttpStatusCode.BadGateway or HttpStatusCode.ServiceUnavailable)) { return response; } // Last attempt exhausted — return the error response as-is. if (attempt == MaxRetries - 1) { return response; } response.Dispose(); await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)), cancellationToken) .ConfigureAwait(false); } // Unreachable when MaxRetries > 0, but satisfies the compiler. throw new InvalidOperationException("Retry loop completed without returning a response."); } private static async Task CloneRequestAsync( HttpRequestMessage original, CancellationToken cancellationToken) { var clone = new HttpRequestMessage(original.Method, original.RequestUri); foreach (var header in original.Headers) { clone.Headers.TryAddWithoutValidation(header.Key, header.Value); } if (original.Content is not null) { var contentBytes = await original.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); clone.Content = new ByteArrayContent(contentBytes); foreach (var header in original.Content.Headers) { clone.Content.Headers.TryAddWithoutValidation(header.Key, header.Value); } } return clone; } }