-
-
Notifications
You must be signed in to change notification settings - Fork 393
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
Eliminate Flurl's HttpClientFactory #736
Comments
May I ask why? I haven't followed the development of the internals but I was under the impression that Flurl would periodically refresh the underlying |
Nope, Flurl doesn't do that, mainly because |
@tmenier With the removal of HTTP client factory in 4.0-pre4, this no longer works: public class UntrustedCertClientFactory : DefaultHttpClientFactory
{
public override HttpMessageHandler CreateMessageHandler()
{
return new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (_, _, _, _) => true
};
}
} Could you provide an example of how one should disable SSL validation in 4.0? I'll keep tinkering with this, it's possible I eventually figure it out. But I think an example would be really helpful. |
@rcdailey Sure. The answer (per #770) is to use 1. Global, when using the "clientless" pattern:FlurlHttp.Clients.WithDefaults(builder => builder.ConfigureInnerHandler(...) 2. Global, when injecting clients via DI:services.AddSingleton<IFlurlClientCache>(_ => new FlurlClientCache()
.WithDefaults(builder => builder.ConfigureInnerHandler(...) Inject instances of 3. Specific named client via DI:services.AddSingleton<IFlurlClientCache>(_ => new FlurlClientCache()
.Add(name, baseUrl, builder => builder.ConfigureInnerHandler(...))); Same usage as above but here you've configured it for a specific client. 4. One-off (recommended only for the simplest of cases):var flurlClient = new FlurlClientBuilder(baseUrl)
.ConfigureInnerHandler(...)
.Build(); |
Thank you, I really appreciate the examples @tmenier. The new configuration strategy is a bit difficult for me to adopt. public class ServarrRequestBuilder : IServarrRequestBuilder
{
private readonly ILogger _log;
private readonly ISettingsProvider _settingsProvider;
private readonly IFlurlClientFactory _clientFactory;
public ServarrRequestBuilder(ILogger log, ISettingsProvider settingsProvider, IFlurlClientFactory clientFactory)
{
_log = log;
_settingsProvider = settingsProvider;
_clientFactory = clientFactory;
}
public IFlurlRequest Request(IServiceConfiguration config, params object[] path)
{
var client = _clientFactory.Get(config.BaseUrl.AppendPathSegments("api", "v3"));
client.Settings = GetClientSettings();
return client.Request(path)
.WithHeader("X-Api-Key", config.ApiKey);
}
private ClientFlurlHttpSettings GetClientSettings()
{
var settings = new ClientFlurlHttpSettings
{
JsonSerializer = new DefaultJsonSerializer(GlobalJsonSerializerSettings.Services)
};
FlurlLogging.SetupLogging(settings, _log);
// ReSharper disable once InvertIf
if (!_settingsProvider.Settings.EnableSslCertificateValidation)
{
_log.Warning(
"Security Risk: Certificate validation is being DISABLED because setting " +
"`enable_ssl_certificate_validation` is set to `false`");
settings.HttpClientFactory = new UntrustedCertClientFactory();
}
return settings;
}
} However, with pre5, this implementation is no longer centralized (as far as I'm able to tell). I have to split off my Then there's Perhaps a solution here is to add an overload of Looks like var client = _clientCache.Get(name);
if (client is null) {
client = _clientCache.Add(name, myBaseUrl);
} |
Here's where I ended up. Seems to work, I think my main feedback is to make the default Used by my own business logic to get/create a client, and set it up for API calls: public class ServarrRequestBuilder : IServarrRequestBuilder
{
private readonly IFlurlClientCache _clientCache;
public ServarrRequestBuilder(IFlurlClientCache clientCache)
{
_clientCache = clientCache;
}
public IFlurlRequest Request(IServiceConfiguration config, params object[] path)
{
var client = _clientCache.Get(config.InstanceName);
client.BaseUrl ??= config.BaseUrl.AppendPathSegments("api", "v3");
return client.Request(path)
.WithHeader("X-Api-Key", config.ApiKey);
}
} Used during creation of the public class FlurlConfigurator
{
private readonly ILogger _log;
private readonly ISettingsProvider _settingsProvider;
public FlurlConfigurator(ILogger log, ISettingsProvider settingsProvider)
{
_log = log;
_settingsProvider = settingsProvider;
}
[SuppressMessage("SonarCloud", "S4830:Server certificates should be verified during SSL/TLS connections")]
[SuppressMessage("Security", "CA5359:Do Not Disable Certificate Validation")]
public void Configure(IFlurlClientBuilder builder)
{
builder.WithSettings(settings =>
{
settings.JsonSerializer = new DefaultJsonSerializer(GlobalJsonSerializerSettings.Services);
FlurlLogging.SetupLogging(settings, _log);
});
builder.ConfigureInnerHandler(handler =>
{
if (!_settingsProvider.Settings.EnableSslCertificateValidation)
{
_log.Warning(
"Security Risk: Certificate validation is being DISABLED because setting " +
"`enable_ssl_certificate_validation` is set to `false`");
handler.SslOptions = new SslClientAuthenticationOptions
{
RemoteCertificateValidationCallback = (_, _, _, _) => true
};
}
});
}
} Relevant part of my composition root (which uses Autofac) to set up the relevant registrations: builder.RegisterType<FlurlConfigurator>();
builder.Register(c =>
{
var config = c.Resolve<FlurlConfigurator>();
return new FlurlClientCache().ConfigureAll(x => config.Configure(x));
})
.As<IFlurlClientCache>()
.SingleInstance(); |
I think what you did here is good. For your injected singleton "provider" thing, you basically just replaced If I understand you correctly, I think you're requesting something similar to ConcurrentDictionary.GetOrAdd that will allow you to add to the cache any time but be guaranteed to initialize just once? That's actually not a bad idea, I'll consider it. But it's worth noting that Flurl never had that functionality, so nothing was actually "lost" between pre3 and pre4. But I think you may have figured that out at some point between your last 2 comments if I read them right. :) |
Yeah, I think you're right about not having lost anything. I realized that I was always setting the settings for clients, even if those settings had already been set before. So I think what we're ending up with here is a new feature request, which might be convenient to add with the rest of the churn happening in v4.0. You already do lazy initialization inside the default cache implementation; it's probably just a matter of exposing that in the public interface so that users can perform one-time initialization at the cache level (for setup involving non-DI injected configuration, like I have). Thanks so much, I'm enjoying the progress you're making! |
Here is an example of how I would probably use such a new feature: var client = _clientCache.GetOrAdd(config.InstanceName, client =>
{
// This lambda is invoked ONCE only when a new client is created. That client
// is passed to the lambda to perform initialization (in this case, setting the base url)
client.BaseUrl = config.BaseUrl.AppendPathSegments("api", "v3");
}); You'll notice I currently work around this by using A lambda might be too generic of a solution here. If there's not much to configure outside of a Base URL and you're concerned that users might abuse this mechanism of configuration (instead of doing it via var client = _clientCache.GetOrAdd(config.InstanceName, config.BaseUrl.AppendPathSegments("api", "v3")); Basically the base URL provided would be assigned to the client only when created instead of the hard-coded With the addition of a |
@tmenier I finally got around to implementing the changes you made in pre6 and overall it is a huge improvement. You did fantastic work. Here are the things I found were improvements since pre5:
I'll provide my "builder" class below so you can see the updated version as well as get an idea of what I'm doing. If I had to give feedback on anything, I'd say that with this change, each time I request a client I'm doing the work to build the base URL each time, even if that value doesn't get used (i.e. when the client already exists). However, this is very minor and not a huge deal. public class ServarrRequestBuilder(
ILogger log,
IFlurlClientCache clientCache,
ISettingsProvider settingsProvider)
: IServarrRequestBuilder
{
public IFlurlRequest Request(IServiceConfiguration config, params object[] path)
{
var client = clientCache.GetOrAdd(
config.InstanceName,
config.BaseUrl.AppendPathSegments("api", "v3"),
Configure);
return client.Request(path)
.WithHeader("X-Api-Key", config.ApiKey);
}
[SuppressMessage("SonarCloud", "S4830:Server certificates should be verified during SSL/TLS connections")]
[SuppressMessage("Security", "CA5359:Do Not Disable Certificate Validation")]
private void Configure(IFlurlClientBuilder builder)
{
builder.WithSettings(settings =>
{
settings.JsonSerializer = new DefaultJsonSerializer(GlobalJsonSerializerSettings.Services);
FlurlLogging.SetupLogging(settings, log);
});
builder.ConfigureInnerHandler(handler =>
{
if (!settingsProvider.Settings.EnableSslCertificateValidation)
{
log.Warning(
"Security Risk: Certificate validation is being DISABLED because setting " +
"`enable_ssl_certificate_validation` is set to `false`");
handler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true;
}
});
}
} |
@rcdailey Great feedback, thank you.
Yeah, that occurred to me, but I do think that base URL is going to be the most common config needing to be set and I like that you can do it directly and skip the config func entirely if you don't need to do anything else. I also think your case might be a little unusual - I suspect in most cases the base URL will come from some config in its entirety and won't need to be "built". You could just use string concat or interpolation there instead if it makes it feel less heavy. :) |
I understand this is a pre-release but the examples provided for Dependency Injection have invalid syntax and will not compile (even after mocking up ConfigureInnerHandler). I tried both 4.0.0-pre4 and 4.0.0-pre6. For example:
The error is: PS - Thank you for the awesome library! |
builder.RegisterType<FlurlClientCache>()
.As<IFlurlClientCache>()
.SingleInstance(); I imagine you already knew this and were just pointing out the updates needed to the interim docs. But thought I'd point it out anyway. |
@jassent Fixed, thanks for catching that. It was more or less a typo - |
Thank you for the update. I think there is still in an issue. This does not compile:
The error is: This is in Visual Studio 17.8.2 NET7 using either 4.0.0-pre4 and 4.0.0-pre6 Updating to this explicit casting works:
And I am not sure how to use ConfigureInnerHandler properly - but that is my problem :) |
@jassent You're on a roll. :) Comment updated. In order for The previous way could be made to work and requires less nesting but isn't chainable, so it would require multiple steps: services.AddSingleton<IFlurlClientCache>(_ => {
var cache = new FlurlClientCache();
cache.Add("testclient", "https://localhost")
.ConfigureInnerHandler(h => { }));
return cache;
}); |
11:18 01/04/2024 OK, I've looked up in the docs and I changed
I have to sign each request by computing a signature and this has to be done on each and every call. I was using What is the new old fashioned way of doing this? |
https://flurl.dev/docs/configuration/#event-handlers Aha! I consulted the web page above and I did:
This made my compiler errors go away. |
NOTE: This issue was superseded by #770. In short, all factories are now gone, and this was an intermediate step in that direction.
Both Flurl and ASP.NET Core define a thing called
IHttpClientFactory
. This can cause confusion, and regardless of which came first, it's not a fight Flurl is likely to win, so I will concede and remove it from Flurl 😃.But the methods it defines -
CreateHttpClient
andCreateMessageHandler
- aren't simply going away, because they are still useful. Instead, they'll be rolled intoIFlurlClientFactory
and used in much the same way when you are not managingFlurlClient
s explicitly. One implication is that you'll no longer be able to assign anHttpClient
-creating factory to aFlurlClient
's settings directly. But I don't believe that's very useful anyway, since you can optionally pass anHttpClient
instance to aFlurlClient
constructor. And if you don't, the globally registeredIFlurlClientFactory
will be invoked to create one, even if you created theFlurlClient
explicitly.These changes should lead to some sweeping simplifications. The broader goal is to get Flurl to a place where it plays more nicely with ASP.NET Core's
IHttpClientFactory
in general.BREAKING:
IHttpClientFactory
and its implementations removed. (Methods moved toIFlurlClientFactory
.)ClientFlurlHttpSettings
class removed.The text was updated successfully, but these errors were encountered: