diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs index 36c57a34..dfa88ba4 100644 --- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs @@ -24,7 +24,8 @@ namespace OpenIddict.Client.SystemNetHttp; /// [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptions, - IConfigureNamedOptions + IConfigureNamedOptions, + IPostConfigureOptions { private readonly IServiceProvider _provider; @@ -73,7 +74,7 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio var settings = _provider.GetRequiredService>().CurrentValue; - options.HttpClientActions.Add(client => + options.HttpClientActions.Add(static client => { // By default, HttpClient uses a default timeout of 100 seconds and allows payloads of up to 2GB. // To help reduce the effects of malicious responses (e.g responses returned at a very slow pace @@ -95,6 +96,61 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio #else var options = _provider.GetRequiredService>(); #endif + // If applicable, add the handler responsible for replaying failed HTTP requests. + // + // Note: on .NET 8.0 and higher, the HTTP error policy is always set + // to null by default and an HTTP resilience pipeline is used instead. + if (options.CurrentValue.HttpErrorPolicy is IAsyncPolicy policy) + { + builder.AdditionalHandlers.Add(new PolicyHttpMessageHandler(policy)); + } + +#if SUPPORTS_HTTP_CLIENT_RESILIENCE + else if (options.CurrentValue.HttpResiliencePipeline is ResiliencePipeline pipeline) + { +#pragma warning disable EXTEXP0001 + builder.AdditionalHandlers.Add(new ResilienceHandler(pipeline)); +#pragma warning restore EXTEXP0001 + } +#endif + }); + + // Register the user-defined HTTP client handler actions. + foreach (var action in settings.HttpClientHandlerActions) + { + options.HttpMessageHandlerBuilderActions.Add(builder => action(registration, + builder.PrimaryHandler as HttpClientHandler ?? + throw new InvalidOperationException(SR.FormatID0373(typeof(HttpClientHandler).FullName)))); + } + } + + /// + public void PostConfigure(string? name, HttpClientFactoryOptions options) + { + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + // Only amend the HTTP client factory options if the instance is managed by OpenIddict. + if (string.IsNullOrEmpty(name) || !TryResolveRegistrationId(name, out string? identifier)) + { + return; + } + + options.HttpMessageHandlerBuilderActions.Insert(0, static builder => + { + // Note: Microsoft.Extensions.Http 9.0+ no longer uses HttpClientHandler as the default instance + // for PrimaryHandler on platforms that support SocketsHttpHandler. Since OpenIddict requires an + // HttpClientHandler instance, it is manually reassigned here if it's not an HttpClientHandler. + if (builder.PrimaryHandler is not HttpClientHandler) + { + builder.PrimaryHandler = new HttpClientHandler(); + } + }); + + options.HttpMessageHandlerBuilderActions.Add(static builder => + { if (builder.PrimaryHandler is not HttpClientHandler handler) { throw new InvalidOperationException(SR.FormatID0373(typeof(HttpClientHandler).FullName)); @@ -135,48 +191,22 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio // // To avoid that, cookies support is explicitly disabled here, for security reasons. handler.UseCookies = false; - - // If applicable, add the handler responsible for replaying failed HTTP requests. - // - // Note: on .NET 8.0 and higher, the HTTP error policy is always set - // to null by default and an HTTP resilience pipeline is used instead. - if (options.CurrentValue.HttpErrorPolicy is IAsyncPolicy policy) - { - builder.AdditionalHandlers.Add(new PolicyHttpMessageHandler(policy)); - } - -#if SUPPORTS_HTTP_CLIENT_RESILIENCE - else if (options.CurrentValue.HttpResiliencePipeline is ResiliencePipeline pipeline) - { -#pragma warning disable EXTEXP0001 - builder.AdditionalHandlers.Add(new ResilienceHandler(pipeline)); -#pragma warning restore EXTEXP0001 - } -#endif }); + } - // Register the user-defined HTTP client handler actions. - foreach (var action in settings.HttpClientHandlerActions) - { - options.HttpMessageHandlerBuilderActions.Add(builder => action(registration, - builder.PrimaryHandler as HttpClientHandler ?? - throw new InvalidOperationException(SR.FormatID0373(typeof(HttpClientHandler).FullName)))); - } + static bool TryResolveRegistrationId(string name, [NotNullWhen(true)] out string? value) + { + var assembly = typeof(OpenIddictClientSystemNetHttpOptions).Assembly.GetName(); - static bool TryResolveRegistrationId(string name, [NotNullWhen(true)] out string? value) + if (!name.StartsWith(assembly.Name!, StringComparison.Ordinal) || + name.Length < assembly.Name!.Length + 1 || + name[assembly.Name.Length] is not ':') { - var assembly = typeof(OpenIddictClientSystemNetHttpOptions).Assembly.GetName(); - - if (!name.StartsWith(assembly.Name!, StringComparison.Ordinal) || - name.Length < assembly.Name!.Length + 1 || - name[assembly.Name.Length] is not ':') - { - value = null; - return false; - } - - value = name[(assembly.Name.Length + 1)..]; - return true; + value = null; + return false; } + + value = name[(assembly.Name.Length + 1)..]; + return true; } } diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs index 4c72d781..5fd499fe 100644 --- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs @@ -43,7 +43,8 @@ public static class OpenIddictClientSystemNetHttpExtensions builder.Services.TryAddEnumerable( [ ServiceDescriptor.Singleton, OpenIddictClientSystemNetHttpConfiguration>(), - ServiceDescriptor.Singleton, OpenIddictClientSystemNetHttpConfiguration>() + ServiceDescriptor.Singleton, OpenIddictClientSystemNetHttpConfiguration>(), + ServiceDescriptor.Singleton, OpenIddictClientSystemNetHttpConfiguration>() ]); return new OpenIddictClientSystemNetHttpBuilder(builder.Services); diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs index 46f2ea1d..7b3bed82 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs @@ -23,7 +23,8 @@ namespace OpenIddict.Validation.SystemNetHttp; /// [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureOptions, - IConfigureNamedOptions + IConfigureNamedOptions, + IPostConfigureOptions { private readonly IServiceProvider _provider; @@ -66,7 +67,7 @@ public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureO var settings = _provider.GetRequiredService>().CurrentValue; - options.HttpClientActions.Add(client => + options.HttpClientActions.Add(static client => { // By default, HttpClient uses a default timeout of 100 seconds and allows payloads of up to 2GB. // To help reduce the effects of malicious responses (e.g responses returned at a very slow pace @@ -88,6 +89,61 @@ public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureO #else var options = _provider.GetRequiredService>(); #endif + // If applicable, add the handler responsible for replaying failed HTTP requests. + // + // Note: on .NET 8.0 and higher, the HTTP error policy is always set + // to null by default and an HTTP resilience pipeline is used instead. + if (options.CurrentValue.HttpErrorPolicy is IAsyncPolicy policy) + { + builder.AdditionalHandlers.Add(new PolicyHttpMessageHandler(policy)); + } + +#if SUPPORTS_HTTP_CLIENT_RESILIENCE + else if (options.CurrentValue.HttpResiliencePipeline is ResiliencePipeline pipeline) + { +#pragma warning disable EXTEXP0001 + builder.AdditionalHandlers.Add(new ResilienceHandler(pipeline)); +#pragma warning restore EXTEXP0001 + } +#endif + }); + + // Register the user-defined HTTP client handler actions. + foreach (var action in settings.HttpClientHandlerActions) + { + options.HttpMessageHandlerBuilderActions.Add(builder => action(builder.PrimaryHandler as HttpClientHandler ?? + throw new InvalidOperationException(SR.FormatID0373(typeof(HttpClientHandler).FullName)))); + } + } + + /// + public void PostConfigure(string? name, HttpClientFactoryOptions options) + { + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + // Only amend the HTTP client factory options if the instance is managed by OpenIddict. + var assembly = typeof(OpenIddictValidationSystemNetHttpOptions).Assembly.GetName(); + if (!string.Equals(name, assembly.Name, StringComparison.Ordinal)) + { + return; + } + + options.HttpMessageHandlerBuilderActions.Insert(0, static builder => + { + // Note: Microsoft.Extensions.Http 9.0+ no longer uses HttpClientHandler as the default instance + // for PrimaryHandler on platforms that support SocketsHttpHandler. Since OpenIddict requires an + // HttpClientHandler instance, it is manually reassigned here if it's not an HttpClientHandler. + if (builder.PrimaryHandler is not HttpClientHandler) + { + builder.PrimaryHandler = new HttpClientHandler(); + } + }); + + options.HttpMessageHandlerBuilderActions.Add(static builder => + { if (builder.PrimaryHandler is not HttpClientHandler handler) { throw new InvalidOperationException(SR.FormatID0373(typeof(HttpClientHandler).FullName)); @@ -128,31 +184,6 @@ public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureO // // To avoid that, cookies support is explicitly disabled here, for security reasons. handler.UseCookies = false; - - // If applicable, add the handler responsible for replaying failed HTTP requests. - // - // Note: on .NET 8.0 and higher, the HTTP error policy is always set - // to null by default and an HTTP resilience pipeline is used instead. - if (options.CurrentValue.HttpErrorPolicy is IAsyncPolicy policy) - { - builder.AdditionalHandlers.Add(new PolicyHttpMessageHandler(policy)); - } - -#if SUPPORTS_HTTP_CLIENT_RESILIENCE - else if (options.CurrentValue.HttpResiliencePipeline is ResiliencePipeline pipeline) - { -#pragma warning disable EXTEXP0001 - builder.AdditionalHandlers.Add(new ResilienceHandler(pipeline)); -#pragma warning restore EXTEXP0001 - } -#endif }); - - // Register the user-defined HTTP client handler actions. - foreach (var action in settings.HttpClientHandlerActions) - { - options.HttpMessageHandlerBuilderActions.Add(builder => action(builder.PrimaryHandler as HttpClientHandler ?? - throw new InvalidOperationException(SR.FormatID0373(typeof(HttpClientHandler).FullName)))); - } } } diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs index d61303dc..73502293 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs @@ -43,7 +43,8 @@ public static class OpenIddictValidationSystemNetHttpExtensions builder.Services.TryAddEnumerable( [ ServiceDescriptor.Singleton, OpenIddictValidationSystemNetHttpConfiguration>(), - ServiceDescriptor.Singleton, OpenIddictValidationSystemNetHttpConfiguration>() + ServiceDescriptor.Singleton, OpenIddictValidationSystemNetHttpConfiguration>(), + ServiceDescriptor.Singleton, OpenIddictValidationSystemNetHttpConfiguration>() ]); return new OpenIddictValidationSystemNetHttpBuilder(builder.Services);