diff --git a/Flurl.sln b/Flurl.sln index bd048cca..1001e2a2 100644 --- a/Flurl.sln +++ b/Flurl.sln @@ -12,18 +12,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{86A5ACB4-F EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flurl.Test", "test\Flurl.Test\Flurl.Test.csproj", "{DF68EB0E-9566-4577-B709-291520383F8D}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{B6BF9238-4541-4E1F-955E-C95F1C2A1F46}" - ProjectSection(SolutionItems) = preProject - appveyor.yml = appveyor.yml - Build\build.cmd = Build\build.cmd - Build\build.sh = Build\build.sh - Build\Flurl.netstandard.sln = Build\Flurl.netstandard.sln - Build\test.cmd = Build\test.cmd - Build\test.coverage.cmd = Build\test.coverage.cmd - Build\test.coverage.sh = Build\test.coverage.sh - Build\test.sh = Build\test.sh - EndProjectSection -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flurl.CodeGen", "src\Flurl.CodeGen\Flurl.CodeGen.csproj", "{BE943E04-705F-42B1-BF95-A0642D9CA51D}" EndProject Global diff --git a/src/Flurl.Http/Configuration/FlurlClientBuilder.cs b/src/Flurl.Http/Configuration/FlurlClientBuilder.cs index 1459f633..aaced9ea 100644 --- a/src/Flurl.Http/Configuration/FlurlClientBuilder.cs +++ b/src/Flurl.Http/Configuration/FlurlClientBuilder.cs @@ -2,31 +2,35 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; +using System.Runtime.Versioning; +using Flurl.Util; namespace Flurl.Http.Configuration { /// /// A builder for configuring IFlurlClient instances. /// - public interface IFlurlClientBuilder + public interface IFlurlClientBuilder : ISettingsContainer, IHeadersContainer { - /// - /// Configure the IFlurlClient's Settings. - /// - IFlurlClientBuilder WithSettings(Action configure); - /// /// Configure the HttpClient wrapped by this IFlurlClient. /// IFlurlClientBuilder ConfigureHttpClient(Action configure); /// - /// Configure the inner-most HttpMessageHandler associated with this IFlurlClient. + /// Configure the inner-most HttpMessageHandler (an instance of HttpClientHandler) associated with this IFlurlClient. /// -#if NETCOREAPP2_1_OR_GREATER - IFlurlClientBuilder ConfigureInnerHandler(Action configure); -#else IFlurlClientBuilder ConfigureInnerHandler(Action configure); + +#if NET + /// + /// Configure a SocketsHttpHandler instead of HttpClientHandler as the inner-most HttpMessageHandler. + /// Note that HttpClientHandler has broader platform support and defers its work to SocketsHttpHandler + /// on supported platforms. It is recommended to explicitly use SocketsHttpHandler ONLY if you + /// need to directly configure its properties that aren't available on HttpClientHandler. + /// + [UnsupportedOSPlatform("browser")] + IFlurlClientBuilder UseSocketsHttpHandler(Action configure); #endif /// @@ -45,36 +49,26 @@ public interface IFlurlClientBuilder /// public class FlurlClientBuilder : IFlurlClientBuilder { - private readonly IFlurlClientFactory _factory; + private IFlurlClientFactory _factory = new DefaultFlurlClientFactory(); + private readonly string _baseUrl; private readonly List> _addMiddleware = new(); - private readonly List> _configSettings = new(); - private readonly List> _configClient = new(); -#if NETCOREAPP2_1_OR_GREATER - private readonly HandlerBuilder _handlerBuilder = new(); -#else - private readonly HandlerBuilder _handlerBuilder = new(); -#endif + private readonly List> _clientConfigs = new(); + private readonly List> _handlerConfigs = new(); - /// - /// Creates a new FlurlClientBuilder. - /// - public FlurlClientBuilder(string baseUrl = null) : this(new DefaultFlurlClientFactory(), baseUrl) { } + /// + public FlurlHttpSettings Settings { get; } = new(); + + /// + public INameValueList Headers { get; } = new NameValueList(false); // header names are case-insensitive https://stackoverflow.com/a/5259004/62600 /// /// Creates a new FlurlClientBuilder. /// - internal FlurlClientBuilder(IFlurlClientFactory factory, string baseUrl) { - _factory = factory; + public FlurlClientBuilder(string baseUrl = null) { _baseUrl = baseUrl; } - /// - public IFlurlClientBuilder WithSettings(Action configure) { - _configSettings.Add(configure); - return this; - } - /// public IFlurlClientBuilder AddMiddleware(Func create) { _addMiddleware.Add(create); @@ -83,23 +77,42 @@ public IFlurlClientBuilder AddMiddleware(Func create) { /// public IFlurlClientBuilder ConfigureHttpClient(Action configure) { - _configClient.Add(configure); + _clientConfigs.Add(configure); return this; } /// -#if NETCOREAPP2_1_OR_GREATER - public IFlurlClientBuilder ConfigureInnerHandler(Action configure) { -#else public IFlurlClientBuilder ConfigureInnerHandler(Action configure) { +#if NET + if (_factory is SocketsHandlerFlurlClientFactory && _handlerConfigs.Any()) + throw new FlurlConfigurationException("ConfigureInnerHandler and UseSocketsHttpHandler cannot be used together. The former configures and instance of HttpClientHandler and would be ignored when switching to SocketsHttpHandler."); #endif - _handlerBuilder.Configs.Add(configure); + _handlerConfigs.Add(h => configure(h as HttpClientHandler)); return this; } +#if NET + /// + public IFlurlClientBuilder UseSocketsHttpHandler(Action configure) { + if (!SocketsHttpHandler.IsSupported) + throw new PlatformNotSupportedException("SocketsHttpHandler is not supported on one or more target platforms."); + + if (_factory is DefaultFlurlClientFactory && _handlerConfigs.Any()) + throw new FlurlConfigurationException("ConfigureInnerHandler and UseSocketsHttpHandler cannot be used together. The former configures and instance of HttpClientHandler and would be ignored when switching to SocketsHttpHandler."); + + if (!(_factory is SocketsHandlerFlurlClientFactory)) + _factory = new SocketsHandlerFlurlClientFactory(); + + _handlerConfigs.Add(h => configure(h as SocketsHttpHandler)); + return this; + } +#endif + /// public IFlurlClient Build() { - var outerHandler = _handlerBuilder.Build(_factory); + var outerHandler = _factory.CreateInnerHandler(); + foreach (var config in _handlerConfigs) + config(outerHandler); foreach (var middleware in Enumerable.Reverse(_addMiddleware).Select(create => create())) { middleware.InnerHandler = outerHandler; @@ -107,31 +120,10 @@ public IFlurlClient Build() { } var httpCli = _factory.CreateHttpClient(outerHandler); - foreach (var config in _configClient) + foreach (var config in _clientConfigs) config(httpCli); - var flurlCli = new FlurlClient(httpCli, _baseUrl); - foreach (var config in _configSettings) - config(flurlCli.Settings); - - return flurlCli; - } - - // helper class to keep those compiler switches from getting too messy - private class HandlerBuilder where T : HttpMessageHandler - { - public List> Configs { get; } = new(); - - public HttpMessageHandler Build(IFlurlClientFactory fac) { - var handler = fac.CreateInnerHandler(); - foreach (var config in Configs) { - if (handler is T h) - config(h); - else - throw new Exception($"ConfigureInnerHandler expected an instance of {typeof(T).Name} but received an instance of {handler.GetType().Name}."); - } - return handler; - } + return new FlurlClient(httpCli, _baseUrl, Settings, Headers); } } } diff --git a/src/Flurl.Http/Configuration/FlurlClientCache.cs b/src/Flurl.Http/Configuration/FlurlClientCache.cs index 5da8da86..a6b74a64 100644 --- a/src/Flurl.Http/Configuration/FlurlClientCache.cs +++ b/src/Flurl.Http/Configuration/FlurlClientCache.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; namespace Flurl.Http.Configuration { @@ -9,52 +10,88 @@ namespace Flurl.Http.Configuration public interface IFlurlClientCache { /// - /// Adds and returns a new IFlurlClient. Call once per client at startup to register and configure a named client. + /// Adds a new IFlurlClient to this cache. Call once per client at startup to register and configure a named client. /// /// Name of the IFlurlClient. Serves as a cache key. Subsequent calls to Get will return this client. /// Optional. The base URL associated with the new client. - /// + /// A builder to further configure the new client. IFlurlClientBuilder Add(string name, string baseUrl = null); /// - /// Gets a named IFlurlClient, creating one if it doesn't exist or has been disposed. + /// Gets a preconfigured named IFlurlClient. /// /// The client name. - /// + /// The cached IFlurlClient. IFlurlClient Get(string name); /// - /// Configuration logic that gets executed for every new IFlurlClient added this case. Good place for things like default - /// settings. Executes before client-specific builder logic. + /// Gets a named IFlurlClient, creating and (optionally) configuring one if it doesn't exist or has been disposed. /// - IFlurlClientCache ConfigureAll(Action configure); + /// The client name. + /// The base URL associated with the new client, if it doesn't exist. + /// Configure the builder associated with the new client, if it doesn't exist. + /// The cached IFlurlClient. + IFlurlClient GetOrAdd(string name, string baseUrl = null, Action configure = null); + + /// + /// Adds initialization logic that gets executed for every new IFlurlClient added this cache. + /// Good place for things like default settings. Executes before client-specific builder logic. + /// Call at startup (or whenever the cache is first created); clients already cached will NOT have this logic applied. + /// + /// This IFlurlCache. + IFlurlClientCache WithDefaults(Action configure); /// /// Removes a named client from this cache. /// - void Remove(string name); + /// This IFlurlCache. + IFlurlClientCache Remove(string name); /// /// Disposes and removes all cached IFlurlClient instances. /// - void Clear(); + /// This IFlurlCache. + IFlurlClientCache Clear(); + } + + /// + /// Extension methods on IFlurlClientCache. + /// + public static class IFlurlClientCacheExtensions + { + /// + /// Adds a new IFlurlClient to this cache. Call once per client at startup to register and configure a named client. + /// Allows configuring via a nested lambda, rather than returning a builder, so multiple Add calls can be fluently chained. + /// + /// This IFlurlCache + /// Name of the IFlurlClient. Serves as a cache key. Subsequent calls to Get will return this client. + /// The base URL associated with the new client. + /// Configure the builder associated with the added client. + /// This IFlurlCache. + public static IFlurlClientCache Add(this IFlurlClientCache cache, string name, string baseUrl, Action configure) { + var builder = cache.Add(name, baseUrl); + configure?.Invoke(builder); + return cache; + } } /// /// Default implementation of IFlurlClientCache. /// - public class FlurlClientCache : IFlurlClientCache { + public class FlurlClientCache : IFlurlClientCache + { private readonly ConcurrentDictionary> _clients = new(); - private readonly IFlurlClientFactory _factory = new DefaultFlurlClientFactory(); - private Action _configureAll; + private readonly List> _defaultConfigs = new(); /// public IFlurlClientBuilder Add(string name, string baseUrl = null) { - if (_clients.ContainsKey(name)) - throw new ArgumentException($"A client named '{name}' was already registered with this factory. AddClient should be called just once per client at startup."); + if (name == null) + throw new ArgumentNullException(nameof(name)); + + var builder = CreateBuilder(baseUrl); + if (!_clients.TryAdd(name, new Lazy(builder.Build))) + throw new ArgumentException($"A client named '{name}' was already registered. Add should be called just once per client at startup."); - var builder = new FlurlClientBuilder(_factory, baseUrl); - _clients[name] = CreateLazyInstance(builder); return builder; } @@ -63,32 +100,56 @@ public virtual IFlurlClient Get(string name) { if (name == null) throw new ArgumentNullException(nameof(name)); - Lazy Create() => CreateLazyInstance(new FlurlClientBuilder(_factory, null)); - return _clients.AddOrUpdate(name, _ => Create(), (_, existing) => existing.Value.IsDisposed ? Create() : existing).Value; + if (!_clients.TryGetValue(name, out var cli)) + throw new ArgumentException($"A client named '{name}' was not found. Either preconfigure the client using Add (typically at startup), or use GetOrAdd to add/configure one on demand when needed."); + + if (cli.Value.IsDisposed) + throw new Exception($"A client named '{name}' was not found but has been disposed and cannot be reused."); + + return cli.Value; } - private Lazy CreateLazyInstance(FlurlClientBuilder builder) { - _configureAll?.Invoke(builder); - return new Lazy(builder.Build); + /// + public IFlurlClient GetOrAdd(string name, string baseUrl = null, Action configure = null) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + Lazy Create() { + var builder = CreateBuilder(baseUrl); + configure?.Invoke(builder); + return new Lazy(builder.Build); + } + + return _clients.AddOrUpdate(name, _ => Create(), (_, existing) => existing.Value.IsDisposed ? Create() : existing).Value; } /// - public IFlurlClientCache ConfigureAll(Action configure) { - _configureAll = configure; + public IFlurlClientCache WithDefaults(Action configure) { + if (configure != null) + _defaultConfigs.Add(configure); return this; } /// - public void Remove(string name) { + public IFlurlClientCache Remove(string name) { if (_clients.TryRemove(name, out var cli) && cli.IsValueCreated && !cli.Value.IsDisposed) cli.Value.Dispose(); + return this; } /// - public void Clear() { + public IFlurlClientCache Clear() { // Remove takes care of disposing too, which is why we don't simply call _clients.Clear foreach (var key in _clients.Keys) Remove(key); + return this; + } + + private IFlurlClientBuilder CreateBuilder(string baseUrl) { + var builder = new FlurlClientBuilder(baseUrl); + foreach (var config in _defaultConfigs) + config(builder); + return builder; } } } diff --git a/src/Flurl.Http/Configuration/FlurlClientFactory.cs b/src/Flurl.Http/Configuration/FlurlClientFactory.cs index 2c27bac5..ab4c16c1 100644 --- a/src/Flurl.Http/Configuration/FlurlClientFactory.cs +++ b/src/Flurl.Http/Configuration/FlurlClientFactory.cs @@ -51,13 +51,6 @@ public virtual HttpClient CreateHttpClient(HttpMessageHandler handler) { /// public virtual HttpMessageHandler CreateInnerHandler() { // Flurl has its own mechanisms for managing cookies and redirects, so we need to disable them in the inner handler. -#if NETCOREAPP2_1_OR_GREATER - var handler = new SocketsHttpHandler { - UseCookies = false, - AllowAutoRedirect = false, - AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate - }; -#else var handler = new HttpClientHandler(); if (handler.SupportsRedirectConfiguration) @@ -70,8 +63,26 @@ public virtual HttpMessageHandler CreateInnerHandler() { try { handler.UseCookies = false; } catch (PlatformNotSupportedException) { } // look out for WASM platforms (#543) -#endif + return handler; } } + +#if NET + /// + /// An implementation of IFlurlClientFactory that uses SocketsHttpHandler on supported platforms. + /// + public class SocketsHandlerFlurlClientFactory : DefaultFlurlClientFactory + { + /// + /// Creates and configures a new SocketsHttpHandler as needed when a new IFlurlClient instance is created. + /// + public override HttpMessageHandler CreateInnerHandler() => new SocketsHttpHandler { + // Flurl has its own mechanisms for managing cookies and redirects, so we need to disable them in the inner handler. + UseCookies = false, + AllowAutoRedirect = false, + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate + }; + } +#endif } diff --git a/src/Flurl.Http/Flurl.Http.csproj b/src/Flurl.Http/Flurl.Http.csproj index 607cdcfe..05f5904e 100644 --- a/src/Flurl.Http/Flurl.Http.csproj +++ b/src/Flurl.Http/Flurl.Http.csproj @@ -4,7 +4,7 @@ 9.0 True Flurl.Http - 4.0.0-pre5 + 4.0.0-pre6 Todd Menier A fluent, portable, testable HTTP client library. https://flurl.dev @@ -25,10 +25,6 @@ true - - true - - bin\Release\Flurl.Http.xml diff --git a/src/Flurl.Http/FlurlClient.cs b/src/Flurl.Http/FlurlClient.cs index d2b2843f..89e56e83 100644 --- a/src/Flurl.Http/FlurlClient.cs +++ b/src/Flurl.Http/FlurlClient.cs @@ -12,7 +12,7 @@ namespace Flurl.Http /// /// Interface defining FlurlClient's contract (useful for mocking and DI) /// - public interface IFlurlClient : IHttpSettingsContainer, IDisposable { + public interface IFlurlClient : ISettingsContainer, IHeadersContainer, IDisposable { /// /// Gets the HttpClient that this IFlurlClient wraps. /// @@ -67,26 +67,32 @@ public FlurlClient(string baseUrl = null) : this(_defaultFactory.Value.CreateHtt /// Flurl's re-implementation of those features may not work properly. /// /// The instantiated HttpClient instance. - /// The base URL associated with this client. - public FlurlClient(HttpClient httpClient, string baseUrl = null) { + /// Optional. The base URL associated with this client. + /// Optional. A pre-initialized collection of settings. + /// Optional. A pre-initialized collection of default request headers. + public FlurlClient(HttpClient httpClient, string baseUrl = null, FlurlHttpSettings settings = null, INameValueList headers = null) { HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); BaseUrl = baseUrl ?? httpClient.BaseAddress?.ToString(); - foreach (var header in httpClient.DefaultRequestHeaders.SelectMany(h => h.Value, (kv, v) => (kv.Key, v))) - Headers.Add(header); - Settings.Timeout = httpClient.Timeout; + Settings = settings ?? new FlurlHttpSettings { Timeout = httpClient.Timeout }; // Timeout can be overridden per request, so don't constrain it by the underlying HttpClient httpClient.Timeout = Timeout.InfiniteTimeSpan; + + Headers = headers ?? new NameValueList(false); // header names are case-insensitive https://stackoverflow.com/a/5259004/62600 + foreach (var header in httpClient.DefaultRequestHeaders.SelectMany(h => h.Value, (kv, v) => (kv.Key, v))) { + if (!Headers.Contains(header.Key)) + Headers.Add(header); + } } /// public string BaseUrl { get; set; } /// - public FlurlHttpSettings Settings { get; } = new(); + public FlurlHttpSettings Settings { get; } /// - public INameValueList Headers { get; } = new NameValueList(false); // header names are case-insensitive https://stackoverflow.com/a/5259004/62600 + public INameValueList Headers { get; } /// public HttpClient HttpClient { get; } @@ -153,10 +159,7 @@ public async Task SendAsync(IFlurlRequest request, HttpCompletio private void SyncHeaders(IFlurlRequest req, HttpRequestMessage reqMsg) { // copy any client-level (default) headers to FlurlRequest - foreach (var header in this.Headers.ToList()) { - if (!req.Headers.Contains(header.Name)) - req.Headers.Add(header.Name, header.Value); - } + FlurlRequest.SyncHeaders(this, req); // copy headers from FlurlRequest to HttpRequestMessage foreach (var header in req.Headers) diff --git a/src/Flurl.Http/FlurlHttp.cs b/src/Flurl.Http/FlurlHttp.cs index 937f5dd4..67f01156 100644 --- a/src/Flurl.Http/FlurlHttp.cs +++ b/src/Flurl.Http/FlurlHttp.cs @@ -26,7 +26,7 @@ public static class FlurlHttp /// /// Gets or creates the IFlurlClient that would be selected for sending the given IFlurlRequest when the clientless pattern is used. /// - public static IFlurlClient GetClientForRequest(IFlurlRequest req) => Clients.Get(_cachingStrategy(req)); + public static IFlurlClient GetClientForRequest(IFlurlRequest req) => Clients.GetOrAdd(_cachingStrategy(req)); /// /// Sets a global caching strategy for getting or creating an IFlurlClient instance when the clientless pattern is used, e.g. url.GetAsync. diff --git a/src/Flurl.Http/FlurlHttpException.cs b/src/Flurl.Http/FlurlHttpException.cs index 8dbfbb45..2d6ff49b 100644 --- a/src/Flurl.Http/FlurlHttpException.cs +++ b/src/Flurl.Http/FlurlHttpException.cs @@ -106,4 +106,15 @@ private static string BuildMessage(FlurlCall call, string expectedFormat) { return msg + ((call == null) ? "." : $": {call}"); } } + + /// + /// An exception that is thrown when Flurl.Http has been misconfigured. + /// + public class FlurlConfigurationException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public FlurlConfigurationException(string message) : base(message) { } + } } \ No newline at end of file diff --git a/src/Flurl.Http/FlurlRequest.cs b/src/Flurl.Http/FlurlRequest.cs index 6d52f2e1..ce11b73a 100644 --- a/src/Flurl.Http/FlurlRequest.cs +++ b/src/Flurl.Http/FlurlRequest.cs @@ -13,7 +13,7 @@ namespace Flurl.Http /// Represents an HTTP request. Can be created explicitly via new FlurlRequest(), fluently via Url.Request(), /// or implicitly when a call is made via methods like Url.GetAsync(). /// - public interface IFlurlRequest : IHttpSettingsContainer + public interface IFlurlRequest : ISettingsContainer, IHeadersContainer { /// /// Gets or sets the IFlurlClient to use when sending the request. @@ -106,6 +106,7 @@ public IFlurlClient Client { set { _client = value; Settings.Parent = _client?.Settings; + SyncHeaders(_client, this); } } @@ -142,6 +143,15 @@ public Task SendAsync(HttpMethod verb, HttpContent content = nul return Client.SendAsync(this, completionOption, cancellationToken); } + internal static void SyncHeaders(IFlurlClient client, IFlurlRequest request) { + if (client == null || request == null) return; + + foreach (var header in client.Headers.ToList()) { + if (!request.Headers.Contains(header.Name)) + request.Headers.Add(header.Name, header.Value); + } + } + private void ApplyCookieJar(CookieJar jar) { _jar = jar; if (jar == null) diff --git a/src/Flurl.Http/HeaderExtensions.cs b/src/Flurl.Http/HeaderExtensions.cs deleted file mode 100644 index b6762246..00000000 --- a/src/Flurl.Http/HeaderExtensions.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Flurl.Util; - -namespace Flurl.Http -{ - /// - /// Fluent extension methods for working with HTTP request headers. - /// - public static class HeaderExtensions - { - /// - /// Sets an HTTP header to be sent with this IFlurlRequest or all requests made with this IFlurlClient. - /// - /// The IFlurlClient or IFlurlRequest. - /// HTTP header name. - /// HTTP header value. - /// This IFlurlClient or IFlurlRequest. - public static T WithHeader(this T clientOrRequest, string name, object value) where T : IHttpSettingsContainer { - if (value == null) - clientOrRequest.Headers.Remove(name); - else - clientOrRequest.Headers.AddOrReplace(name, value.ToInvariantString().Trim()); - return clientOrRequest; - } - - /// - /// Sets HTTP headers based on property names/values of the provided object, or keys/values if object is a dictionary, to be sent with this IFlurlRequest or all requests made with this IFlurlClient. - /// - /// The IFlurlClient or IFlurlRequest. - /// Names/values of HTTP headers to set. Typically an anonymous object or IDictionary. - /// If true, underscores in property names will be replaced by hyphens. Default is true. - /// This IFlurlClient or IFlurlRequest. - public static T WithHeaders(this T clientOrRequest, object headers, bool replaceUnderscoreWithHyphen = true) where T : IHttpSettingsContainer { - if (headers == null) - return clientOrRequest; - - // underscore replacement only applies when object properties are parsed to kv pairs - replaceUnderscoreWithHyphen = replaceUnderscoreWithHyphen && !(headers is string) && !(headers is IEnumerable); - - foreach (var kv in headers.ToKeyValuePairs()) { - var key = replaceUnderscoreWithHyphen ? kv.Key.Replace("_", "-") : kv.Key; - clientOrRequest.WithHeader(key, kv.Value); - } - - return clientOrRequest; - } - - /// - /// Sets HTTP authorization header according to Basic Authentication protocol to be sent with this IFlurlRequest or all requests made with this IFlurlClient. - /// - /// The IFlurlClient or IFlurlRequest. - /// Username of authenticating user. - /// Password of authenticating user. - /// This IFlurlClient or IFlurlRequest. - public static T WithBasicAuth(this T clientOrRequest, string username, string password) where T : IHttpSettingsContainer { - // http://stackoverflow.com/questions/14627399/setting-authorization-header-of-httpclient - var encodedCreds = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}")); - return clientOrRequest.WithHeader("Authorization", $"Basic {encodedCreds}"); - } - - /// - /// Sets HTTP authorization header with acquired bearer token according to OAuth 2.0 specification to be sent with this IFlurlRequest or all requests made with this IFlurlClient. - /// - /// The IFlurlClient or IFlurlRequest. - /// The acquired bearer token to pass. - /// This IFlurlClient or IFlurlRequest. - public static T WithOAuthBearerToken(this T clientOrRequest, string token) where T : IHttpSettingsContainer { - return clientOrRequest.WithHeader("Authorization", $"Bearer {token}"); - } - } -} diff --git a/src/Flurl.Http/IHeadersContainer.cs b/src/Flurl.Http/IHeadersContainer.cs new file mode 100644 index 00000000..366f1b3f --- /dev/null +++ b/src/Flurl.Http/IHeadersContainer.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections; +using System.Text; +using Flurl.Util; + +namespace Flurl.Http +{ + /// + /// A common interface for Flurl.Http objects that contain a collection of request headers. + /// + public interface IHeadersContainer + { + /// + /// A collection of request headers. + /// + INameValueList Headers { get; } + } + + /// + /// Fluent extension methods for working with HTTP request headers. + /// + public static class HeaderExtensions + { + /// + /// Sets an HTTP header associated with this request or client. + /// + /// Object containing request headers. + /// HTTP header name. + /// HTTP header value. + /// This headers container. + public static T WithHeader(this T obj, string name, object value) where T : IHeadersContainer { + if (value == null) + obj.Headers.Remove(name); + else + obj.Headers.AddOrReplace(name, value.ToInvariantString().Trim()); + return obj; + } + + /// + /// Sets HTTP headers based on property names/values of the provided object, or keys/values if object is a dictionary, associated with this request or client. + /// + /// Object containing request headers. + /// Names/values of HTTP headers to set. Typically an anonymous object or IDictionary. + /// If true, underscores in property names will be replaced by hyphens. Default is true. + /// This headers container. + public static T WithHeaders(this T obj, object headers, bool replaceUnderscoreWithHyphen = true) where T : IHeadersContainer { + if (headers == null) + return obj; + + // underscore replacement only applies when object properties are parsed to kv pairs + replaceUnderscoreWithHyphen = replaceUnderscoreWithHyphen && !(headers is string) && !(headers is IEnumerable); + + foreach (var kv in headers.ToKeyValuePairs()) { + var key = replaceUnderscoreWithHyphen ? kv.Key.Replace("_", "-") : kv.Key; + obj.WithHeader(key, kv.Value); + } + + return obj; + } + + /// + /// Sets HTTP authorization header according to Basic Authentication protocol associated with this request or client. + /// + /// Object containing request headers. + /// Username of authenticating user. + /// Password of authenticating user. + /// This headers container. + public static T WithBasicAuth(this T obj, string username, string password) where T : IHeadersContainer { + // http://stackoverflow.com/questions/14627399/setting-authorization-header-of-httpclient + var encodedCreds = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}")); + return obj.WithHeader("Authorization", $"Basic {encodedCreds}"); + } + + /// + /// Sets HTTP authorization header with acquired bearer token according to OAuth 2.0 specification associated with this request or client. + /// + /// Object containing request headers. + /// The acquired bearer token to pass. + /// This headers container. + public static T WithOAuthBearerToken(this T obj, string token) where T : IHeadersContainer { + return obj.WithHeader("Authorization", $"Bearer {token}"); + } + } +} diff --git a/src/Flurl.Http/IHttpSettingsContainer.cs b/src/Flurl.Http/IHttpSettingsContainer.cs deleted file mode 100644 index 50696c41..00000000 --- a/src/Flurl.Http/IHttpSettingsContainer.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using Flurl.Http.Configuration; -using Flurl.Util; - -namespace Flurl.Http -{ - /// - /// Defines stateful aspects (headers, cookies, etc) common to both IFlurlClient and IFlurlRequest - /// - public interface IHttpSettingsContainer - { - /// - /// Gets the FlurlHttpSettings object used by this client or request. - /// - FlurlHttpSettings Settings { get; } - - /// - /// Collection of headers sent on this request or all requests using this client. - /// - INameValueList Headers { get; } - } -} diff --git a/src/Flurl.Http/SettingsExtensions.cs b/src/Flurl.Http/ISettingsContainer.cs similarity index 69% rename from src/Flurl.Http/SettingsExtensions.cs rename to src/Flurl.Http/ISettingsContainer.cs index b5c3a06f..2c8a6d0b 100644 --- a/src/Flurl.Http/SettingsExtensions.cs +++ b/src/Flurl.Http/ISettingsContainer.cs @@ -7,50 +7,50 @@ namespace Flurl.Http { /// - /// Fluent extension methods for tweaking FlurlHttpSettings + /// A common interface for Flurl.Http objects that contain a collection of request settings. /// - public static class SettingsExtensions + public interface ISettingsContainer { /// - /// Change FlurlHttpSettings for this IFlurlClient. + /// A collection request settings. /// - /// The IFlurlClient. - /// Action defining the settings changes. - /// The IFlurlClient with the modified Settings - public static IFlurlClient WithSettings(this IFlurlClient client, Action action) { - action(client.Settings); - return client; - } + FlurlHttpSettings Settings { get; } + } + /// + /// Fluent extension methods for tweaking FlurlHttpSettings + /// + public static class SettingsExtensions + { /// - /// Change FlurlHttpSettings for this IFlurlRequest. + /// Change FlurlHttpSettings for this request, client, or test context. /// - /// The IFlurlRequest. + /// Object containing settings. /// Action defining the settings changes. - /// The IFlurlRequest with the modified Settings - public static IFlurlRequest WithSettings(this IFlurlRequest request, Action action) { - action(request.Settings); - return request; + /// This settings container. + public static T WithSettings(this T obj, Action action) where T : ISettingsContainer { + action(obj.Settings); + return obj; } /// - /// Sets the timeout for this IFlurlRequest or all requests made with this IFlurlClient. + /// Sets the timeout for this request, client, or test context. /// - /// The IFlurlClient or IFlurlRequest. + /// Object containing settings. /// Time to wait before the request times out. - /// This IFlurlClient or IFlurlRequest. - public static T WithTimeout(this T obj, TimeSpan timespan) where T : IHttpSettingsContainer { + /// This settings container. + public static T WithTimeout(this T obj, TimeSpan timespan) where T : ISettingsContainer { obj.Settings.Timeout = timespan; return obj; } /// - /// Sets the timeout for this IFlurlRequest or all requests made with this IFlurlClient. + /// Sets the timeout for this request, client, or test context. /// - /// The IFlurlClient or IFlurlRequest. + /// Object containing settings. /// Seconds to wait before the request times out. - /// This IFlurlClient or IFlurlRequest. - public static T WithTimeout(this T obj, int seconds) where T : IHttpSettingsContainer { + /// This settings container. + public static T WithTimeout(this T obj, int seconds) where T : ISettingsContainer { obj.Settings.Timeout = TimeSpan.FromSeconds(seconds); return obj; } @@ -58,10 +58,10 @@ public static T WithTimeout(this T obj, int seconds) where T : IHttpSettingsC /// /// Adds a pattern representing an HTTP status code or range of codes which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown. /// - /// The IFlurlClient or IFlurlRequest. + /// Object containing settings. /// Examples: "3xx", "100,300,600", "100-299,6xx" - /// This IFlurlClient or IFlurlRequest. - public static T AllowHttpStatus(this T obj, string pattern) where T : IHttpSettingsContainer { + /// This settings container. + public static T AllowHttpStatus(this T obj, string pattern) where T : ISettingsContainer { if (!string.IsNullOrWhiteSpace(pattern)) { var current = obj.Settings.AllowedHttpStatusRange; if (string.IsNullOrWhiteSpace(current)) @@ -75,10 +75,10 @@ public static T AllowHttpStatus(this T obj, string pattern) where T : IHttpSe /// /// Adds an which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown. /// - /// The IFlurlClient or IFlurlRequest. + /// Object containing settings. /// Examples: HttpStatusCode.NotFound - /// This IFlurlClient or IFlurlRequest. - public static T AllowHttpStatus(this T obj, params HttpStatusCode[] statusCodes) where T : IHttpSettingsContainer { + /// This settings container. + public static T AllowHttpStatus(this T obj, params HttpStatusCode[] statusCodes) where T : ISettingsContainer { var pattern = string.Join(",", statusCodes.Select(c => (int)c)); return AllowHttpStatus(obj, pattern); } @@ -86,9 +86,9 @@ public static T AllowHttpStatus(this T obj, params HttpStatusCode[] statusCod /// /// Prevents a FlurlHttpException from being thrown on any completed response, regardless of the HTTP status code. /// - /// The IFlurlClient or IFlurlRequest. - /// This IFlurlClient or IFlurlRequest. - public static T AllowAnyHttpStatus(this T obj) where T : IHttpSettingsContainer { + /// Object containing settings. + /// This settings container. + public static T AllowAnyHttpStatus(this T obj) where T : ISettingsContainer { obj.Settings.AllowedHttpStatusRange = "*"; return obj; } @@ -96,10 +96,10 @@ public static T AllowAnyHttpStatus(this T obj) where T : IHttpSettingsContain /// /// Configures whether redirects are automatically followed. /// - /// The IFlurlClient or IFlurlRequest. + /// Object containing settings. /// true if Flurl should automatically send a new request to the redirect URL, false if it should not. - /// This IFlurlClient or IFlurlRequest. - public static T WithAutoRedirect(this T obj, bool enabled) where T : IHttpSettingsContainer { + /// This settings container. + public static T WithAutoRedirect(this T obj, bool enabled) where T : ISettingsContainer { obj.Settings.Redirects.Enabled = enabled; return obj; } @@ -107,7 +107,8 @@ public static T WithAutoRedirect(this T obj, bool enabled) where T : IHttpSet /// /// Sets a callback that is invoked immediately before every HTTP request is sent. /// - public static T BeforeCall(this T obj, Action act) where T : IHttpSettingsContainer { + /// This settings container. + public static T BeforeCall(this T obj, Action act) where T : ISettingsContainer { obj.Settings.BeforeCall = act; return obj; } @@ -115,7 +116,8 @@ public static T BeforeCall(this T obj, Action act) where T : IHttp /// /// Sets a callback that is invoked asynchronously immediately before every HTTP request is sent. /// - public static T BeforeCall(this T obj, Func act) where T : IHttpSettingsContainer { + /// This settings container. + public static T BeforeCall(this T obj, Func act) where T : ISettingsContainer { obj.Settings.BeforeCallAsync = act; return obj; } @@ -123,7 +125,8 @@ public static T BeforeCall(this T obj, Func act) where T : I /// /// Sets a callback that is invoked immediately after every HTTP response is received. /// - public static T AfterCall(this T obj, Action act) where T : IHttpSettingsContainer { + /// This settings container. + public static T AfterCall(this T obj, Action act) where T : ISettingsContainer { obj.Settings.AfterCall = act; return obj; } @@ -131,7 +134,8 @@ public static T AfterCall(this T obj, Action act) where T : IHttpS /// /// Sets a callback that is invoked asynchronously immediately after every HTTP response is received. /// - public static T AfterCall(this T obj, Func act) where T : IHttpSettingsContainer { + /// This settings container. + public static T AfterCall(this T obj, Func act) where T : ISettingsContainer { obj.Settings.AfterCallAsync = act; return obj; } @@ -140,7 +144,8 @@ public static T AfterCall(this T obj, Func act) where T : IH /// Sets a callback that is invoked when an error occurs during any HTTP call, including when any non-success /// HTTP status code is returned in the response. Response should be null-checked if used in the event handler. /// - public static T OnError(this T obj, Action act) where T : IHttpSettingsContainer { + /// This settings container. + public static T OnError(this T obj, Action act) where T : ISettingsContainer { obj.Settings.OnError = act; return obj; } @@ -149,7 +154,8 @@ public static T OnError(this T obj, Action act) where T : IHttpSet /// Sets a callback that is invoked asynchronously when an error occurs during any HTTP call, including when any non-success /// HTTP status code is returned in the response. Response should be null-checked if used in the event handler. /// - public static T OnError(this T obj, Func act) where T : IHttpSettingsContainer { + /// This settings container. + public static T OnError(this T obj, Func act) where T : ISettingsContainer { obj.Settings.OnErrorAsync = act; return obj; } @@ -159,7 +165,8 @@ public static T OnError(this T obj, Func act) where T : IHtt /// You can inspect/manipulate the call.Redirect object to determine what will happen next. /// An auto-redirect will only happen if call.Redirect.Follow is true upon exiting the callback. /// - public static T OnRedirect(this T obj, Action act) where T : IHttpSettingsContainer { + /// This settings container. + public static T OnRedirect(this T obj, Action act) where T : ISettingsContainer { obj.Settings.OnRedirect = act; return obj; } @@ -169,7 +176,8 @@ public static T OnRedirect(this T obj, Action act) where T : IHttp /// You can inspect/manipulate the call.Redirect object to determine what will happen next. /// An auto-redirect will only happen if call.Redirect.Follow is true upon exiting the callback. /// - public static T OnRedirect(this T obj, Func act) where T : IHttpSettingsContainer { + /// This settings container. + public static T OnRedirect(this T obj, Func act) where T : ISettingsContainer { obj.Settings.OnRedirectAsync = act; return obj; } diff --git a/src/Flurl.Http/Testing/HttpTest.cs b/src/Flurl.Http/Testing/HttpTest.cs index 43a0c0a6..cf221944 100644 --- a/src/Flurl.Http/Testing/HttpTest.cs +++ b/src/Flurl.Http/Testing/HttpTest.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Net.Http; using Flurl.Http.Configuration; namespace Flurl.Http.Testing @@ -13,7 +12,7 @@ namespace Flurl.Http.Testing /// queue, call log, and assertion helpers for use in Arrange/Act/Assert style tests. /// [Serializable] - public class HttpTest : HttpTestSetup, IDisposable + public class HttpTest : HttpTestSetup, ISettingsContainer, IDisposable { private readonly ConcurrentQueue _calls = new(); private readonly List _filteredSetups = new(); @@ -38,16 +37,6 @@ public HttpTest() : base(new FlurlHttpSettings()) { /// public IReadOnlyList CallLog => new ReadOnlyCollection(_calls.ToList()); - /// - /// Change FlurlHttpSettings for the scope of this HttpTest. - /// - /// Action defining the settings changes. - /// This HttpTest - public HttpTest WithSettings(Action action) { - action(Settings); - return this; - } - /// /// Fluently creates and returns a new request-specific test setup. /// diff --git a/src/Flurl.Http/Testing/HttpTestSetup.cs b/src/Flurl.Http/Testing/HttpTestSetup.cs index 4439ce30..698339b0 100644 --- a/src/Flurl.Http/Testing/HttpTestSetup.cs +++ b/src/Flurl.Http/Testing/HttpTestSetup.cs @@ -16,7 +16,7 @@ namespace Flurl.Http.Testing /// public abstract class HttpTestSetup { - private readonly List> _responses = new List>(); + private readonly List> _responses = new(); private int _respIndex = 0; private bool _allowRealHttp = false; diff --git a/src/Flurl/Flurl.csproj b/src/Flurl/Flurl.csproj index b51bd95a..efbe4fb0 100644 --- a/src/Flurl/Flurl.csproj +++ b/src/Flurl/Flurl.csproj @@ -24,10 +24,6 @@ true - - true - - bin\Release\Flurl.xml diff --git a/test/Flurl.Test/Http/FlurlClientBuilderTests.cs b/test/Flurl.Test/Http/FlurlClientBuilderTests.cs index 1fe97be5..efdcae5f 100644 --- a/test/Flurl.Test/Http/FlurlClientBuilderTests.cs +++ b/test/Flurl.Test/Http/FlurlClientBuilderTests.cs @@ -12,13 +12,6 @@ namespace Flurl.Test.Http [TestFixture] public class FlurlClientBuilderTests { - [Test] - public void can_configure_settings() { - var builder = new FlurlClientBuilder(); - var cli = builder.WithSettings(s => s.HttpVersion = "3.0").Build(); - Assert.AreEqual("3.0", cli.Settings.HttpVersion); - } - [Test] public void can_configure_HttpClient() { var builder = new FlurlClientBuilder(); @@ -51,18 +44,37 @@ public async Task can_add_middleware() { } [Test] - public void inner_hanlder_is_SocketsHttpHandler_when_supported() { + public void uses_HttpClientHandler_by_default() { HttpMessageHandler handler = null; new FlurlClientBuilder() .ConfigureInnerHandler(h => handler = h) .Build(); + Assert.IsInstanceOf(handler); + } + #if NET + [Test] + public void can_use_SocketsHttpHandler() { + HttpMessageHandler handler = null; + new FlurlClientBuilder() + .UseSocketsHttpHandler(h => handler = h) + .Build(); Assert.IsInstanceOf(handler); -#else - Assert.IsInstanceOf(handler); -#endif } + [Test] + public void cannot_mix_handler_types() { + Assert.Throws(() => new FlurlClientBuilder() + .ConfigureInnerHandler(_ => { }) + .UseSocketsHttpHandler(_ => { })); + + // reverse + Assert.Throws(() => new FlurlClientBuilder() + .UseSocketsHttpHandler(_ => { }) + .ConfigureInnerHandler(_ => { })); + } +#endif + class BlockingHandler : DelegatingHandler { private readonly string _msg; diff --git a/test/Flurl.Test/Http/FlurlClientCacheTests.cs b/test/Flurl.Test/Http/FlurlClientCacheTests.cs index b44ce4ed..17494d6e 100644 --- a/test/Flurl.Test/Http/FlurlClientCacheTests.cs +++ b/test/Flurl.Test/Http/FlurlClientCacheTests.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Flurl.Http; using Flurl.Http.Configuration; using NUnit.Framework; @@ -29,14 +26,32 @@ public void can_add_and_get_client() { } [Test] - public void can_get_unconfigured_client() { + public void can_get_or_add_client() { var cache = new FlurlClientCache(); - var cli = cache.Get("foo"); + IFlurlClient firstCli = null; + var configCalls = 0; + + for (var i = 0; i < 10; i++) { + var cli = cache.GetOrAdd("foo", "https://api.com", _ => configCalls++); + if (i == 0) + firstCli = cli; + else + Assert.AreSame(firstCli, cli); + } + + Assert.AreEqual("https://api.com", firstCli.BaseUrl); + Assert.AreEqual(1, configCalls); + } + + [Test] + public void can_get_or_add_unconfigured_client() { + var cache = new FlurlClientCache(); + var cli = cache.GetOrAdd("foo"); + Assert.IsNull(cli.BaseUrl); Assert.AreEqual(100, cli.Settings.Timeout.Value.TotalSeconds); - - Assert.AreSame(cli, cache.Get("foo"), "should reuse same-named clients."); - Assert.AreNotSame(cli, cache.Get("bar"), "different-named clients should be different instances."); + Assert.AreSame(cli, cache.GetOrAdd("foo"), "should reuse same-named clients."); + Assert.AreNotSame(cli, cache.GetOrAdd("bar"), "different-named clients should be different instances."); } [Test] @@ -47,21 +62,21 @@ public void cannot_add_same_name_twice() { } [Test] - public void can_configure_all() { + public void can_configure_defaults() { var cache = new FlurlClientCache() - .ConfigureAll(b => b.WithSettings(s => s.Timeout = TimeSpan.FromSeconds(123))); + .WithDefaults(b => b.Settings.Timeout = TimeSpan.FromSeconds(123)); - var cli1 = cache.Get("foo"); + var cli1 = cache.GetOrAdd("foo"); cache.Add("bar").WithSettings(s => { s.Timeout = TimeSpan.FromSeconds(456); }); - cache.ConfigureAll(b => b.WithSettings(s => { + cache.WithDefaults(b => b.WithSettings(s => { s.Timeout = TimeSpan.FromSeconds(789); })); - var cli2 = cache.Get("bar"); - var cli3 = cache.Get("buzz"); + var cli2 = cache.GetOrAdd("bar"); + var cli3 = cache.GetOrAdd("buzz"); Assert.AreEqual(123, cli1.Settings.Timeout.Value.TotalSeconds, "fetched the client before changing the defaults, so original should stick"); Assert.AreEqual(456, cli2.Settings.Timeout.Value.TotalSeconds, "client-specific settings should always win, even if defaults were changed after"); @@ -77,7 +92,9 @@ public void can_remove() { var cli1 = cache.Get("foo"); var cli2 = cache.Get("foo"); cache.Remove("foo"); - var cli3 = cache.Get("foo"); + + Assert.Throws(() => cache.Get("foo")); + var cli3 = cache.GetOrAdd("foo"); Assert.AreSame(cli1, cli2); Assert.AreNotSame(cli1, cli3); diff --git a/test/Flurl.Test/Http/FlurlClientTests.cs b/test/Flurl.Test/Http/FlurlClientTests.cs index 87dddc34..2936b08b 100644 --- a/test/Flurl.Test/Http/FlurlClientTests.cs +++ b/test/Flurl.Test/Http/FlurlClientTests.cs @@ -70,6 +70,13 @@ public void can_create_request_with_base_url_and_no_segments() { Assert.AreEqual("http://myapi.com", req.Url.ToString()); } + [Test] + public void can_create_request_with_Uri() { + var uri = new System.Uri("http://www.mysite.com/foo?x=1"); + var req = new FlurlClient().Request(uri); + Assert.AreEqual(uri.ToString(), req.Url.ToString()); + } + [Test] public void cannot_send_invalid_request() { var cli = new FlurlClient(); diff --git a/test/Flurl.Test/Http/HeadersTests.cs b/test/Flurl.Test/Http/HeadersTests.cs new file mode 100644 index 00000000..684ddd40 --- /dev/null +++ b/test/Flurl.Test/Http/HeadersTests.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Flurl.Http; +using Flurl.Http.Configuration; +using Flurl.Http.Testing; +using NUnit.Framework; + +namespace Flurl.Test.Http +{ + /// + /// A Headers collection is available on IFlurlRequest, IFlurlClient, and IFlurlBuilder. + /// This abstract class allows the same tests to be run against all 3. + /// + public abstract class HeadersTestsBase where T : IHeadersContainer + { + protected abstract T CreateContainer(); + protected abstract IFlurlRequest GetRequest(T container); + + [Test] + public void can_set_header() { + var c = CreateContainer(); + c.WithHeader("a", 1); + Assert.AreEqual(("a", "1"), GetRequest(c).Headers.Single()); + } + + [Test] + public void can_set_headers_from_anon_object() { + var c = CreateContainer(); + // null values shouldn't be added + c.WithHeaders(new { a = "b", one = 2, three = (object)null }); + + var req = GetRequest(c); + Assert.AreEqual(2, req.Headers.Count); + Assert.IsTrue(req.Headers.Contains("a", "b")); + Assert.IsTrue(req.Headers.Contains("one", "2")); + } + + [Test] + public void can_remove_header_by_setting_null() { + var c = CreateContainer(); + c.WithHeaders(new { a = 1, b = 2 }); + Assert.AreEqual(2, GetRequest(c).Headers.Count); + + c.WithHeader("b", null); + + var req = GetRequest(c); + Assert.AreEqual(1, req.Headers.Count); + Assert.IsFalse(req.Headers.Contains("b")); + } + + [Test] + public void can_set_headers_from_dictionary() { + var c = CreateContainer(); + c.WithHeaders(new Dictionary { { "a", "b" }, { "one", 2 } }); + + var req = GetRequest(c); + Assert.AreEqual(2, req.Headers.Count); + Assert.IsTrue(req.Headers.Contains("a", "b")); + Assert.IsTrue(req.Headers.Contains("one", "2")); + } + + [Test] + public void underscores_in_properties_convert_to_hyphens_in_header_names() { + var c = CreateContainer(); + c.WithHeaders(new { User_Agent = "Flurl", Cache_Control = "no-cache" }); + + var req = GetRequest(c); + Assert.IsTrue(req.Headers.Contains("User-Agent")); + Assert.IsTrue(req.Headers.Contains("Cache-Control")); + + // make sure we can disable the behavior + c.WithHeaders(new { no_i_really_want_underscores = "foo" }, false); + Assert.IsTrue(GetRequest(c).Headers.Contains("no_i_really_want_underscores")); + + // dictionaries don't get this behavior since you can use hyphens explicitly + c.WithHeaders(new Dictionary { { "exclude_dictionaries", "bar" } }); + Assert.IsTrue(GetRequest(c).Headers.Contains("exclude_dictionaries")); + + // same with strings + c.WithHeaders("exclude_strings=123"); + Assert.IsTrue(GetRequest(c).Headers.Contains("exclude_strings")); + } + + [Test] + public void header_names_are_case_insensitive() { + var c = CreateContainer(); + c.WithHeader("a", 1).WithHeader("A", 2); + + var req = GetRequest(c); + Assert.AreEqual(1, req.Headers.Count); + Assert.AreEqual("A", req.Headers.Single().Name); + Assert.AreEqual("2", req.Headers.Single().Value); + } + + [Test] // #623 + public async Task header_values_are_trimmed() { + var c = CreateContainer(); + c.WithHeader("a", " 1 \t\r\n"); + c.Headers.Add("b", " 2 "); + + var req = GetRequest(c); + Assert.AreEqual(2, req.Headers.Count); + Assert.AreEqual("1", req.Headers[0].Value); + // Not trimmed when added directly to Headers collection (implementation seemed like overkill), + // but below we'll make sure it happens on HttpRequestMessage when request is sent. + Assert.AreEqual(" 2 ", req.Headers[1].Value); + + using var test = new HttpTest(); + await GetRequest(c).GetAsync(); + var sentHeaders = test.CallLog[0].HttpRequestMessage.Headers; + Assert.AreEqual("1", sentHeaders.GetValues("a").Single()); + Assert.AreEqual("2", sentHeaders.GetValues("b").Single()); + } + + [Test] + public void can_setup_oauth_bearer_token() { + var c = CreateContainer(); + c.WithOAuthBearerToken("mytoken"); + + var req = GetRequest(c); + Assert.AreEqual(1, req.Headers.Count); + Assert.IsTrue(req.Headers.Contains("Authorization", "Bearer mytoken")); + } + + [Test] + public void can_setup_basic_auth() { + var c = CreateContainer(); + c.WithBasicAuth("user", "pass"); + + var req = GetRequest(c); + Assert.AreEqual(1, req.Headers.Count); + Assert.IsTrue(req.Headers.Contains("Authorization", "Basic dXNlcjpwYXNz")); + } + } + + [TestFixture] + public class RequestHeadersTests : HeadersTestsBase + { + protected override IFlurlRequest CreateContainer() => new FlurlRequest("http://api.com"); + protected override IFlurlRequest GetRequest(IFlurlRequest req) => req; + } + + [TestFixture] + public class ClientHeadersTests : HeadersTestsBase + { + protected override IFlurlClient CreateContainer() => new FlurlClient(); + protected override IFlurlRequest GetRequest(IFlurlClient cli) => cli.Request("http://api.com"); + } + + [TestFixture] + public class ClientBuilderHeadersTests : HeadersTestsBase + { + protected override IFlurlClientBuilder CreateContainer() => new FlurlClientBuilder(); + protected override IFlurlRequest GetRequest(IFlurlClientBuilder builder) => builder.Build().Request("http://api.com"); + } +} diff --git a/test/Flurl.Test/Http/SettingsExtensionsTests.cs b/test/Flurl.Test/Http/SettingsExtensionsTests.cs deleted file mode 100644 index 96cb170e..00000000 --- a/test/Flurl.Test/Http/SettingsExtensionsTests.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Flurl.Http; -using Flurl.Http.Testing; -using NUnit.Framework; - -namespace Flurl.Test.Http -{ - // IFlurlClient and IFlurlRequest both implement IHttpSettingsContainer, which defines a number - // of settings-related extension methods. This abstract test class allows those methods to be - // tested against both both client-level and request-level implementations. - public abstract class SettingsExtensionsTests where T : IHttpSettingsContainer - { - protected abstract T GetSettingsContainer(); - protected abstract IFlurlRequest GetRequest(T sc); - - [Test] - public void can_set_timeout() { - var sc = GetSettingsContainer().WithTimeout(TimeSpan.FromSeconds(15)); - Assert.AreEqual(TimeSpan.FromSeconds(15), sc.Settings.Timeout); - } - - [Test] - public void can_set_timeout_in_seconds() { - var sc = GetSettingsContainer().WithTimeout(15); - Assert.AreEqual(sc.Settings.Timeout, TimeSpan.FromSeconds(15)); - } - - [Test] - public void can_set_header() { - var sc = GetSettingsContainer().WithHeader("a", 1); - Assert.AreEqual(("a", "1"), sc.Headers.Single()); - } - - [Test] - public void can_set_headers_from_anon_object() { - // null values shouldn't be added - var sc = GetSettingsContainer().WithHeaders(new { a = "b", one = 2, three = (object)null }); - Assert.AreEqual(2, sc.Headers.Count); - Assert.IsTrue(sc.Headers.Contains("a", "b")); - Assert.IsTrue(sc.Headers.Contains("one", "2")); - } - - [Test] - public void can_remove_header_by_setting_null() { - var sc = GetSettingsContainer().WithHeaders(new { a = 1, b = 2 }); - Assert.AreEqual(2, sc.Headers.Count); - sc.WithHeader("b", null); - Assert.AreEqual(1, sc.Headers.Count); - Assert.IsFalse(sc.Headers.Contains("b")); - } - - [Test] - public void can_set_headers_from_dictionary() { - var sc = GetSettingsContainer().WithHeaders(new Dictionary { { "a", "b" }, { "one", 2 } }); - Assert.AreEqual(2, sc.Headers.Count); - Assert.IsTrue(sc.Headers.Contains("a", "b")); - Assert.IsTrue(sc.Headers.Contains("one", "2")); - } - - [Test] - public void underscores_in_properties_convert_to_hyphens_in_header_names() { - var sc = GetSettingsContainer().WithHeaders(new { User_Agent = "Flurl", Cache_Control = "no-cache" }); - Assert.IsTrue(sc.Headers.Contains("User-Agent")); - Assert.IsTrue(sc.Headers.Contains("Cache-Control")); - - // make sure we can disable the behavior - sc.WithHeaders(new { no_i_really_want_underscores = "foo" }, false); - Assert.IsTrue(sc.Headers.Contains("no_i_really_want_underscores")); - - // dictionaries don't get this behavior since you can use hyphens explicitly - sc.WithHeaders(new Dictionary { { "exclude_dictionaries", "bar" } }); - Assert.IsTrue(sc.Headers.Contains("exclude_dictionaries")); - - // same with strings - sc.WithHeaders("exclude_strings=123"); - Assert.IsTrue(sc.Headers.Contains("exclude_strings")); - } - - [Test] - public void header_names_are_case_insensitive() { - var sc = GetSettingsContainer().WithHeader("a", 1).WithHeader("A", 2); - Assert.AreEqual(1, sc.Headers.Count); - Assert.AreEqual("A", sc.Headers.Single().Name); - Assert.AreEqual("2", sc.Headers.Single().Value); - } - - [Test] // #623 - public async Task header_values_are_trimmed() { - var sc = GetSettingsContainer().WithHeader("a", " 1 \t\r\n"); - sc.Headers.Add("b", " 2 "); - - Assert.AreEqual(2, sc.Headers.Count); - Assert.AreEqual("1", sc.Headers[0].Value); - // Not trimmed when added directly to Headers collection (implementation seemed like overkill), - // but below we'll make sure it happens on HttpRequestMessage when request is sent. - Assert.AreEqual(" 2 ", sc.Headers[1].Value); - - using (var test = new HttpTest()) { - await GetRequest(sc).GetAsync(); - var sentHeaders = test.CallLog[0].HttpRequestMessage.Headers; - Assert.AreEqual("1", sentHeaders.GetValues("a").Single()); - Assert.AreEqual("2", sentHeaders.GetValues("b").Single()); - } - } - - [Test] - public void can_setup_oauth_bearer_token() { - var sc = GetSettingsContainer().WithOAuthBearerToken("mytoken"); - Assert.AreEqual(1, sc.Headers.Count); - Assert.IsTrue(sc.Headers.Contains("Authorization", "Bearer mytoken")); - } - - [Test] - public void can_setup_basic_auth() { - var sc = GetSettingsContainer().WithBasicAuth("user", "pass"); - Assert.AreEqual(1, sc.Headers.Count); - Assert.IsTrue(sc.Headers.Contains("Authorization", "Basic dXNlcjpwYXNz")); - } - - [Test] - public async Task can_allow_specific_http_status() { - using var test = new HttpTest(); - test.RespondWith("Nothing to see here", 404); - var sc = GetSettingsContainer().AllowHttpStatus(HttpStatusCode.Conflict, HttpStatusCode.NotFound); - await GetRequest(sc).DeleteAsync(); // no exception = pass - } - - [Test] - public async Task allow_specific_http_status_also_allows_2xx() { - using var test = new HttpTest(); - test.RespondWith("I'm just an innocent 2xx, I should never fail!", 201); - var sc = GetSettingsContainer().AllowHttpStatus(HttpStatusCode.Conflict, HttpStatusCode.NotFound); - await GetRequest(sc).GetAsync(); // no exception = pass - } - - [Test] - public void can_clear_non_success_status() { - using var test = new HttpTest(); - test.RespondWith("I'm a teapot", 418); - // allow 4xx - var sc = GetSettingsContainer().AllowHttpStatus("4xx"); - // but then disallow it - sc.Settings.AllowedHttpStatusRange = null; - Assert.ThrowsAsync(async () => await GetRequest(sc).GetAsync()); - } - - [Test] - public async Task can_allow_any_http_status() { - using var test = new HttpTest(); - test.RespondWith("epic fail", 500); - try { - var sc = GetSettingsContainer().AllowAnyHttpStatus(); - var result = await GetRequest(sc).GetAsync(); - Assert.AreEqual(500, result.StatusCode); - } - catch (Exception) { - Assert.Fail("Exception should not have been thrown."); - } - } - } - - [TestFixture, Parallelizable] - public class ClientSettingsExtensionsTests : SettingsExtensionsTests - { - protected override IFlurlClient GetSettingsContainer() => new FlurlClient(); - protected override IFlurlRequest GetRequest(IFlurlClient client) => client.Request("http://api.com"); - - [Test] - public void WithUrl_shares_client_but_not_Url() { - var cli = new FlurlClient().WithHeader("myheader", "123"); - var req1 = cli.Request("http://www.api.com/for-req1"); - var req2 = cli.Request("http://www.api.com/for-req2"); - var req3 = cli.Request("http://www.api.com/for-req3"); - - CollectionAssert.AreEquivalent(req1.Headers, req2.Headers); - CollectionAssert.AreEquivalent(req1.Headers, req3.Headers); - var urls = new[] { req1, req2, req3 }.Select(c => c.Url.ToString()); - CollectionAssert.AllItemsAreUnique(urls); - } - - [Test] - public void can_use_uri_with_WithUrl() { - var uri = new System.Uri("http://www.mysite.com/foo?x=1"); - var req = new FlurlClient().Request(uri); - Assert.AreEqual(uri.ToString(), req.Url.ToString()); - } - - [Test] - public void can_override_settings_fluently() { - using (var test = new HttpTest()) { - var cli = new FlurlClient().WithSettings(s => s.AllowedHttpStatusRange = "*"); - test.RespondWith("epic fail", 500); - var req = "http://www.api.com".WithSettings(c => c.AllowedHttpStatusRange = "2xx"); - req.Client = cli; // client-level settings shouldn't win - Assert.ThrowsAsync(async () => await req.GetAsync()); - } - } - } - - [TestFixture, Parallelizable] - public class RequestSettingsExtensionsTests : SettingsExtensionsTests - { - protected override IFlurlRequest GetSettingsContainer() => new FlurlRequest("http://api.com"); - protected override IFlurlRequest GetRequest(IFlurlRequest req) => req; - } -} \ No newline at end of file diff --git a/test/Flurl.Test/Http/SettingsTests.cs b/test/Flurl.Test/Http/SettingsTests.cs index 64e2f7cd..9ccbe4da 100644 --- a/test/Flurl.Test/Http/SettingsTests.cs +++ b/test/Flurl.Test/Http/SettingsTests.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Net; using System.Threading.Tasks; using Flurl.Http; using Flurl.Http.Configuration; @@ -9,22 +10,23 @@ namespace Flurl.Test.Http { /// - /// FlurlHttpSettings are available at the test, client, and request level. This abstract class - /// allows the same tests to be run against settings at all 4 levels. + /// A Settings collection is available on IFlurlRequest, IFlurlClient, IFlurlBuilder, and HttpTest. + /// This abstract class allows the same tests to be run against all 4. /// - public abstract class SettingsTestsBase + public abstract class SettingsTestsBase where T : ISettingsContainer { - protected abstract FlurlHttpSettings GetSettings(); - protected abstract IFlurlRequest GetRequest(); + protected abstract T CreateContainer(); + protected abstract IFlurlRequest GetRequest(T container); [Test] public async Task can_set_http_version() { - Assert.AreEqual("1.1", GetSettings().HttpVersion); // default - using var test = new HttpTest(); - GetSettings().HttpVersion = "2.0"; - var req = GetRequest(); + var c = CreateContainer(); + Assert.AreEqual("1.1", c.Settings.HttpVersion); // default + + c.Settings.HttpVersion = "2.0"; + var req = GetRequest(c); Assert.AreEqual("2.0", req.Settings.HttpVersion); Version versionUsed = null; @@ -37,17 +39,18 @@ await req [Test] public void cannot_set_invalid_http_version() { - Assert.Throws(() => GetSettings().HttpVersion = "foo"); + Assert.Throws(() => CreateContainer().Settings.HttpVersion = "foo"); } [Test] public async Task can_allow_non_success_status() { using var test = new HttpTest(); - GetSettings().AllowedHttpStatusRange = "4xx"; + var c = CreateContainer(); + c.Settings.AllowedHttpStatusRange = "4xx"; test.RespondWith("I'm a teapot", 418); try { - var result = await GetRequest().GetAsync(); + var result = await GetRequest(c).GetAsync(); Assert.AreEqual(418, result.StatusCode); } catch (Exception) { @@ -61,12 +64,13 @@ public async Task can_set_pre_callback() { using var test = new HttpTest(); test.RespondWith("ok"); - GetSettings().BeforeCall = call => { + var c = CreateContainer(); + c.Settings.BeforeCall = call => { Assert.Null(call.Response); // verifies that callback is running before HTTP call is made callbackCalled = true; }; Assert.IsFalse(callbackCalled); - await GetRequest().GetAsync(); + await GetRequest(c).GetAsync(); Assert.IsTrue(callbackCalled); } @@ -76,12 +80,13 @@ public async Task can_set_post_callback() { using var test = new HttpTest(); test.RespondWith("ok"); - GetSettings().AfterCall = call => { + var c = CreateContainer(); + c.Settings.AfterCall = call => { Assert.NotNull(call.Response); // verifies that callback is running after HTTP call is made callbackCalled = true; }; Assert.IsFalse(callbackCalled); - await GetRequest().GetAsync(); + await GetRequest(c).GetAsync(); Assert.IsTrue(callbackCalled); } @@ -92,14 +97,15 @@ public async Task can_set_error_callback(bool markExceptionHandled) { using var test = new HttpTest(); test.RespondWith("server error", 500); - GetSettings().OnError = call => { + var c = CreateContainer(); + c.Settings.OnError = call => { Assert.NotNull(call.Response); // verifies that callback is running after HTTP call is made callbackCalled = true; call.ExceptionHandled = markExceptionHandled; }; Assert.IsFalse(callbackCalled); try { - await GetRequest().GetAsync(); + await GetRequest(c).GetAsync(); Assert.IsTrue(callbackCalled, "OnError was never called"); Assert.IsTrue(markExceptionHandled, "ExceptionHandled was marked false in callback, but exception was not propagated."); } @@ -113,12 +119,13 @@ public async Task can_set_error_callback(bool markExceptionHandled) { public async Task can_disable_exception_behavior() { using var test = new HttpTest(); - GetSettings().OnError = call => { + var c = CreateContainer(); + c.Settings.OnError = call => { call.ExceptionHandled = true; }; test.RespondWith("server error", 500); try { - var result = await GetRequest().GetAsync(); + var result = await GetRequest(c).GetAsync(); Assert.AreEqual(500, result.StatusCode); } catch (FlurlHttpException) { @@ -128,22 +135,27 @@ public async Task can_disable_exception_behavior() { [Test] public void can_reset_defaults() { - GetSettings().JsonSerializer = null; - GetSettings().Redirects.Enabled = false; - GetSettings().BeforeCall = (call) => Console.WriteLine("Before!"); - GetSettings().Redirects.MaxAutoRedirects = 5; + var c = CreateContainer(); + + c.Settings.JsonSerializer = null; + c.Settings.Redirects.Enabled = false; + c.Settings.BeforeCall = (call) => Console.WriteLine("Before!"); + c.Settings.Redirects.MaxAutoRedirects = 5; - Assert.IsNull(GetSettings().JsonSerializer); - Assert.IsFalse(GetSettings().Redirects.Enabled); - Assert.IsNotNull(GetSettings().BeforeCall); - Assert.AreEqual(5, GetSettings().Redirects.MaxAutoRedirects); + var req = GetRequest(c); - GetSettings().ResetDefaults(); + Assert.IsNull(req.Settings.JsonSerializer); + Assert.IsFalse(req.Settings.Redirects.Enabled); + Assert.IsNotNull(req.Settings.BeforeCall); + Assert.AreEqual(5, req.Settings.Redirects.MaxAutoRedirects); - Assert.That(GetSettings().JsonSerializer is DefaultJsonSerializer); - Assert.IsTrue(GetSettings().Redirects.Enabled); - Assert.IsNull(GetSettings().BeforeCall); - Assert.AreEqual(10, GetSettings().Redirects.MaxAutoRedirects); + c.Settings.ResetDefaults(); + req = GetRequest(c); + + Assert.That(req.Settings.JsonSerializer is DefaultJsonSerializer); + Assert.IsTrue(req.Settings.Redirects.Enabled); + Assert.IsNull(req.Settings.BeforeCall); + Assert.AreEqual(10, req.Settings.Redirects.MaxAutoRedirects); } [Test] // #256 @@ -160,21 +172,110 @@ public async Task explicit_content_type_header_is_not_overridden() { Assert.AreEqual(new[] { "application/json-patch+json; utf-8" }, h.GetValues("Content-Type")); Assert.AreEqual(new[] { "10" }, h.GetValues("Content-Length")); } + + [Test] + public void can_set_timeout() { + var c = CreateContainer().WithTimeout(TimeSpan.FromSeconds(15)); + var req = GetRequest(c); + Assert.AreEqual(TimeSpan.FromSeconds(15), req.Settings.Timeout); + } + + [Test] + public void can_set_timeout_in_seconds() { + var c = CreateContainer().WithTimeout(15); + var req = GetRequest(c); + Assert.AreEqual(req.Settings.Timeout, TimeSpan.FromSeconds(15)); + } + + [Test] + public async Task can_allow_specific_http_status() { + using var test = new HttpTest(); + test.RespondWith("Nothing to see here", 404); + var c = CreateContainer().AllowHttpStatus(HttpStatusCode.Conflict, HttpStatusCode.NotFound); + await GetRequest(c).DeleteAsync(); // no exception = pass + } + + [Test] + public async Task allow_specific_http_status_also_allows_2xx() { + using var test = new HttpTest(); + test.RespondWith("I'm just an innocent 2xx, I should never fail!", 201); + var c = CreateContainer().AllowHttpStatus(HttpStatusCode.Conflict, HttpStatusCode.NotFound); + await GetRequest(c).GetAsync(); // no exception = pass + } + + [Test] + public void can_clear_non_success_status() { + using var test = new HttpTest(); + test.RespondWith("I'm a teapot", 418); + // allow 4xx + var c = CreateContainer().AllowHttpStatus("4xx"); + // but then disallow it + c.Settings.AllowedHttpStatusRange = null; + Assert.ThrowsAsync(async () => await GetRequest(c).GetAsync()); + } + + [Test] + public async Task can_allow_any_http_status() { + using var test = new HttpTest(); + test.RespondWith("epic fail", 500); + try { + var c = CreateContainer().AllowAnyHttpStatus(); + var result = await GetRequest(c).GetAsync(); + Assert.AreEqual(500, result.StatusCode); + } + catch (Exception) { + Assert.Fail("Exception should not have been thrown."); + } + } } [TestFixture] - public class HttpTestSettingsTests : SettingsTestsBase + public class RequestSettingsTests : SettingsTestsBase { - private HttpTest _test; + protected override IFlurlRequest CreateContainer() => new FlurlRequest("http://api.com"); + protected override IFlurlRequest GetRequest(IFlurlRequest req) => req; - [SetUp] - public void CreateTest() => _test = new HttpTest(); + [Test] + public void request_gets_default_settings_when_no_client() { + var req = new FlurlRequest(); + Assert.IsNull(req.Client); + Assert.IsNull(req.Url); + Assert.IsInstanceOf(req.Settings.JsonSerializer); + } - [TearDown] - public void DisposeTest() => _test.Dispose(); + [Test] + public void can_override_settings_fluently() { + using var test = new HttpTest(); + var cli = new FlurlClient().WithSettings(s => s.AllowedHttpStatusRange = "*"); + test.RespondWith("epic fail", 500); + var req = "http://www.api.com".WithSettings(c => c.AllowedHttpStatusRange = "2xx"); + req.Client = cli; // client-level settings shouldn't win + Assert.ThrowsAsync(async () => await req.GetAsync()); + } + } - protected override FlurlHttpSettings GetSettings() => HttpTest.Current.Settings; - protected override IFlurlRequest GetRequest() => new FlurlRequest("http://api.com"); + [TestFixture] + public class ClientSettingsTests : SettingsTestsBase + { + protected override IFlurlClient CreateContainer() => new FlurlClient(); + protected override IFlurlRequest GetRequest(IFlurlClient cli) => cli.Request("http://api.com"); + } + + [TestFixture] + public class ClientBuilderSettingsTests : SettingsTestsBase + { + protected override IFlurlClientBuilder CreateContainer() => new FlurlClientBuilder(); + protected override IFlurlRequest GetRequest(IFlurlClientBuilder builder) => builder.Build().Request("http://api.com"); + } + + [TestFixture] + public class HttpTestSettingsTests : SettingsTestsBase + { + protected override HttpTest CreateContainer() => HttpTest.Current ?? new HttpTest(); + protected override IFlurlRequest GetRequest(HttpTest container) => new FlurlRequest("http://api.com"); + + [TearDown] + public void DisposeTest() => HttpTest.Current?.Dispose(); [Test] // #246 public void test_settings_dont_override_request_settings_when_not_set_explicitily() { @@ -201,30 +302,4 @@ private class FakeSerializer : ISerializer public T Deserialize(Stream stream) => default; } } - - [TestFixture] - public class ClientSettingsTests : SettingsTestsBase - { - private readonly Lazy _client = new Lazy(() => new FlurlClient()); - - protected override FlurlHttpSettings GetSettings() => _client.Value.Settings; - protected override IFlurlRequest GetRequest() => _client.Value.Request("http://api.com"); - } - - [TestFixture] - public class RequestSettingsTests : SettingsTestsBase - { - private readonly Lazy _req = new Lazy(() => new FlurlRequest("http://api.com")); - - protected override FlurlHttpSettings GetSettings() => _req.Value.Settings; - protected override IFlurlRequest GetRequest() => _req.Value; - - [Test] - public void request_gets_default_settings_when_no_client() { - var req = new FlurlRequest(); - Assert.IsNull(req.Client); - Assert.IsNull(req.Url); - Assert.IsInstanceOf(req.Settings.JsonSerializer); - } - } }