diff --git a/Directory.Packages.props b/Directory.Packages.props
index 2b4bd77b..5e885b1f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -36,6 +36,7 @@
+
diff --git a/EssentialCSharp.Chat.Shared/EssentialCSharp.Chat.Common.csproj b/EssentialCSharp.Chat.Shared/EssentialCSharp.Chat.Common.csproj
index e600d10f..f51ecef9 100644
--- a/EssentialCSharp.Chat.Shared/EssentialCSharp.Chat.Common.csproj
+++ b/EssentialCSharp.Chat.Shared/EssentialCSharp.Chat.Common.csproj
@@ -6,6 +6,7 @@
+
diff --git a/EssentialCSharp.Chat.Shared/Extensions/ServiceCollectionExtensions.cs b/EssentialCSharp.Chat.Shared/Extensions/ServiceCollectionExtensions.cs
index 059a6d13..24dfdb53 100644
--- a/EssentialCSharp.Chat.Shared/Extensions/ServiceCollectionExtensions.cs
+++ b/EssentialCSharp.Chat.Shared/Extensions/ServiceCollectionExtensions.cs
@@ -4,8 +4,10 @@
using EssentialCSharp.Chat.Common.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Http.Resilience;
using Microsoft.SemanticKernel;
using Npgsql;
+using Polly;
namespace EssentialCSharp.Chat.Common.Extensions;
@@ -38,6 +40,9 @@ public static IServiceCollection AddAzureOpenAIServices(
var endpoint = new Uri(aiOptions.Endpoint);
+ // Configure HTTP resilience for Azure OpenAI requests
+ ConfigureAzureOpenAIResilience(services);
+
// Register Azure OpenAI services with Managed Identity authentication
#pragma warning disable SKEXP0010 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
services.AddAzureOpenAIChatClient(
@@ -71,6 +76,65 @@ public static IServiceCollection AddAzureOpenAIServices(
return services;
}
+ ///
+ /// Configures HTTP resilience (retry, circuit breaker, timeout) for Azure OpenAI HTTP clients.
+ /// This handles rate limiting (HTTP 429) and transient errors with exponential backoff.
+ ///
+ /// The service collection to configure
+ ///
+ /// This method configures resilience for ALL HTTP clients created via IHttpClientFactory.
+ ///
+ /// IMPORTANT: The Semantic Kernel's AddAzureOpenAI* extension methods (used in this class)
+ /// do NOT expose options to configure specific named or typed HttpClients. The internal
+ /// implementation creates HttpClient instances through IHttpClientFactory without
+ /// providing hooks for per-client configuration. Therefore, ConfigureHttpClientDefaults
+ /// is the ONLY way to apply resilience to Azure OpenAI clients when using Semantic Kernel.
+ ///
+ /// For Azure OpenAI services specifically, the resilience configuration:
+ /// - Retries HTTP 429 (rate limit), 408 (timeout), and 5xx errors
+ /// - Respects Retry-After headers from Azure OpenAI
+ /// - Uses exponential backoff with jitter
+ /// - Implements circuit breaker pattern
+ ///
+ /// This is appropriate for applications that primarily use Azure OpenAI services.
+ /// The retry policies are reasonable for most HTTP APIs and should not negatively
+ /// impact other HTTP clients like hCaptcha or Mailjet.
+ ///
+ private static void ConfigureAzureOpenAIResilience(IServiceCollection services)
+ {
+ // Configure resilience for all HTTP clients created via IHttpClientFactory
+ // The Semantic Kernel's AddAzureOpenAI* methods do not support named/typed
+ // HttpClient configuration, so ConfigureHttpClientDefaults is required.
+ services.ConfigureHttpClientDefaults(httpClientBuilder =>
+ {
+ httpClientBuilder.AddStandardResilienceHandler(options =>
+ {
+ // Configure retry strategy for rate limiting and transient errors
+ options.Retry.MaxRetryAttempts = 5;
+ options.Retry.Delay = TimeSpan.FromSeconds(2);
+ options.Retry.BackoffType = DelayBackoffType.Exponential;
+ options.Retry.UseJitter = true;
+
+ // The standard resilience handler already handles:
+ // - HTTP 429 (Too Many Requests / Rate Limit)
+ // - HTTP 408 (Request Timeout)
+ // - HTTP 5xx (Server Errors)
+ // - Respects Retry-After header automatically
+
+ // Configure circuit breaker to prevent overwhelming the service
+ options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);
+ options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(15);
+ options.CircuitBreaker.FailureRatio = 0.2; // Break if 20% of requests fail
+
+ // Configure timeout for individual attempts
+ options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(30);
+
+ // Configure total timeout for all retry attempts
+ options.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(3);
+ });
+ });
+ }
+
///
/// Adds Azure OpenAI and related AI services to the service collection using configuration
///
@@ -183,6 +247,9 @@ public static IServiceCollection AddAzureOpenAIServicesWithApiKey(
var endpoint = new Uri(aiOptions.Endpoint);
+ // Configure HTTP resilience for Azure OpenAI requests
+ ConfigureAzureOpenAIResilience(services);
+
// Register Azure OpenAI services with API key authentication
#pragma warning disable SKEXP0010 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
services.AddAzureOpenAIChatClient(
diff --git a/EssentialCSharp.Web/Extensions/LoggerExtensions.cs b/EssentialCSharp.Web/Extensions/LoggerExtensions.cs
index aa183e8b..4658aa3e 100644
--- a/EssentialCSharp.Web/Extensions/LoggerExtensions.cs
+++ b/EssentialCSharp.Web/Extensions/LoggerExtensions.cs
@@ -4,7 +4,7 @@ namespace EssentialCSharp.Web.Extensions;
internal static partial class LoggerExtensions
{
- [LoggerMessage(Level = LogLevel.Debug, EventId = 1, Message = "Successful captcha with response of: '{JsonResult}'")]
+ [LoggerMessage(Level = LogLevel.Debug, EventId = 1, Message = "Successful captcha with response")]
public static partial void HomeControllerSuccessfulCaptchaResponse(
- this ILogger logger, JsonResult jsonResult);
+ this ILogger logger);
}