-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Make SocketsHttpHandler the default primary handler from HttpClientFactory #101808
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,18 +5,22 @@ | |||||||||
| using System.Collections.Generic; | ||||||||||
| using System.Diagnostics.CodeAnalysis; | ||||||||||
| using System.Net.Http; | ||||||||||
| using System.Threading; | ||||||||||
| using Microsoft.Extensions.DependencyInjection; | ||||||||||
| using Microsoft.Extensions.Options; | ||||||||||
|
|
||||||||||
| namespace Microsoft.Extensions.Http | ||||||||||
| { | ||||||||||
| internal sealed class DefaultHttpMessageHandlerBuilder : HttpMessageHandlerBuilder | ||||||||||
| { | ||||||||||
| private HttpMessageHandler? _primaryHandler; | ||||||||||
| private string? _name; | ||||||||||
|
|
||||||||||
| public DefaultHttpMessageHandlerBuilder(IServiceProvider services) | ||||||||||
| { | ||||||||||
| Services = services; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private string? _name; | ||||||||||
|
|
||||||||||
| [DisallowNull] | ||||||||||
| public override string? Name | ||||||||||
| { | ||||||||||
|
|
@@ -28,7 +32,11 @@ public override string? Name | |||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| public override HttpMessageHandler PrimaryHandler { get; set; } = new HttpClientHandler(); | ||||||||||
| public override HttpMessageHandler PrimaryHandler | ||||||||||
| { | ||||||||||
| get => _primaryHandler ??= CreatePrimaryHandler(); | ||||||||||
| set => _primaryHandler = value; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| public override IList<DelegatingHandler> AdditionalHandlers { get; } = new List<DelegatingHandler>(); | ||||||||||
|
|
||||||||||
|
|
@@ -44,5 +52,36 @@ public override HttpMessageHandler Build() | |||||||||
|
|
||||||||||
| return CreateHandlerPipeline(PrimaryHandler, AdditionalHandlers); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| #pragma warning disable CA1822, CA1859 // Mark members as static, Use concrete types when possible for improved performance | ||||||||||
| private HttpMessageHandler CreatePrimaryHandler() | ||||||||||
| #pragma warning restore CA1822, CA1859 | ||||||||||
| { | ||||||||||
| #if NET | ||||||||||
| // On platforms where SocketsHttpHandler is supported, HttpClientHandler is a thin wrapper | ||||||||||
| // around it. By using SocketsHttpHandler directly, we can avoid the overhead of the wrapper, | ||||||||||
| // but more importantly, we can configure it to limit the lifetime of its pooled connections | ||||||||||
| // to match the requested lifetime of the handler itself. That way, if/when someone holds on | ||||||||||
| // to a resulting HttpClient for a prolonged period of time, it'll still benefit from connection | ||||||||||
| // recycling, and without needing to tear down and reconstitute the rest of the handler pipeline. | ||||||||||
| if (SocketsHttpHandler.IsSupported) | ||||||||||
| { | ||||||||||
| SocketsHttpHandler handler = new(); | ||||||||||
|
|
||||||||||
| if (Services.GetService<IOptionsMonitor<HttpClientFactoryOptions>>() is IOptionsMonitor<HttpClientFactoryOptions> optionsMonitor) | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I need to think more about this part. I need to check whether it would give us an expected result in all circumstances. Also I need to make sure the I'll return to it tomorrow.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry I still don't have an answer to that. It's in my pipeline, I will reply by Monday.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, so the answers:
👍
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @stephentoub Is the check I'd vote for simply injecting
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Mainly*, yes. It's guaranteed to have been registered? Where does that happen? *Since IServiceProvider was being injected into the constructor rather than individual resources, this also seemed to be the more consistent approach, but I can change it if desired.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It's there because we need to flow it to configuration callbacks, bc we don't really know what services the callbacks will need at this point.
Yes, it happens in the "base" Line 31 in c6bfb06
which in turn has runtime/src/libraries/Microsoft.Extensions.Options/src/OptionsServiceCollectionExtensions.cs Line 28 in c6bfb06
Constructor injection will align with the other usages in
and Line 23 in c6bfb06
So I believe it will be better to do it the same way, that being said -- I don't want to hold back this PR only for that change; I can do it myself later as a follow-up.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ok, thanks. I started making the change, but it ends up adding a lot more ifdefs (only wanting a field for the options if this NET, only having the ctor parameter if it's NET, etc.), so I'll leave it up to you as a follow-up if you decide you still want to go that route. |
||||||||||
| { | ||||||||||
| TimeSpan lifetime = optionsMonitor.Get(_name).HandlerLifetime; | ||||||||||
| if (lifetime >= TimeSpan.Zero) | ||||||||||
| { | ||||||||||
| handler.PooledConnectionLifetime = lifetime; | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| return handler; | ||||||||||
| } | ||||||||||
| #endif | ||||||||||
|
|
||||||||||
| return new HttpClientHandler(); | ||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had concerns that there are certain workarounds like the one existing in gRPC, where the user sets the handler explicitly to
null!(it was also discussed a bit here https://github.com/dotnet/runtime/pull/90272/files#r1293537394)https://github.com/grpc/grpc-dotnet/blob/854e0fd3d0a7da8507ae0f4bf3adac129fb7e10b/src/Grpc.Net.ClientFactory/GrpcClientServiceExtensions.cs#L349-L354
I'll try to use grep app to check whether there are any other users with similar hacks; and I'll also chat with @JamesNK when he's back to see whether he would be able to change the hack to e.g. ConfigureHttpClientDefaults
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So you don't need to worry about it anymore: grpc/grpc-dotnet#2445
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @JamesNK!