Skip to content

Commit

Permalink
#773 refactor IHttpSettingsContainer
Browse files Browse the repository at this point in the history
  • Loading branch information
Todd committed Nov 3, 2023
1 parent a3c9d90 commit 8f4fe46
Show file tree
Hide file tree
Showing 14 changed files with 480 additions and 471 deletions.
27 changes: 9 additions & 18 deletions src/Flurl.Http/Configuration/FlurlClientBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,15 @@
using System.Linq;
using System.Net.Http;
using System.Runtime.Versioning;
using Flurl.Util;

namespace Flurl.Http.Configuration
{
/// <summary>
/// A builder for configuring IFlurlClient instances.
/// </summary>
public interface IFlurlClientBuilder
public interface IFlurlClientBuilder : ISettingsContainer, IHeadersContainer
{
/// <summary>
/// Configure the IFlurlClient's Settings.
/// </summary>
IFlurlClientBuilder WithSettings(Action<FlurlHttpSettings> configure);

/// <summary>
/// Configure the HttpClient wrapped by this IFlurlClient.
/// </summary>
Expand Down Expand Up @@ -57,23 +53,22 @@ public class FlurlClientBuilder : IFlurlClientBuilder

private readonly string _baseUrl;
private readonly List<Func<DelegatingHandler>> _addMiddleware = new();
private readonly List<Action<FlurlHttpSettings>> _settingsConfigs = new();
private readonly List<Action<HttpClient>> _clientConfigs = new();
private readonly List<Action<HttpMessageHandler>> _handlerConfigs = new();

/// <inheritdoc />
public FlurlHttpSettings Settings { get; } = new();

/// <inheritdoc />
public INameValueList<string> Headers { get; } = new NameValueList<string>(false); // header names are case-insensitive https://stackoverflow.com/a/5259004/62600

/// <summary>
/// Creates a new FlurlClientBuilder.
/// </summary>
public FlurlClientBuilder(string baseUrl = null) {
_baseUrl = baseUrl;
}

/// <inheritdoc />
public IFlurlClientBuilder WithSettings(Action<FlurlHttpSettings> configure) {
_settingsConfigs.Add(configure);
return this;
}

/// <inheritdoc />
public IFlurlClientBuilder AddMiddleware(Func<DelegatingHandler> create) {
_addMiddleware.Add(create);
Expand Down Expand Up @@ -128,11 +123,7 @@ public IFlurlClient Build() {
foreach (var config in _clientConfigs)
config(httpCli);

var flurlCli = new FlurlClient(httpCli, _baseUrl);
foreach (var config in _settingsConfigs)
config(flurlCli.Settings);

return flurlCli;
return new FlurlClient(httpCli, _baseUrl, Settings, Headers);
}
}
}
27 changes: 15 additions & 12 deletions src/Flurl.Http/FlurlClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Flurl.Http
/// <summary>
/// Interface defining FlurlClient's contract (useful for mocking and DI)
/// </summary>
public interface IFlurlClient : IHttpSettingsContainer, IDisposable {
public interface IFlurlClient : ISettingsContainer, IHeadersContainer, IDisposable {
/// <summary>
/// Gets the HttpClient that this IFlurlClient wraps.
/// </summary>
Expand Down Expand Up @@ -67,26 +67,32 @@ public FlurlClient(string baseUrl = null) : this(_defaultFactory.Value.CreateHtt
/// Flurl's re-implementation of those features may not work properly.
/// </summary>
/// <param name="httpClient">The instantiated HttpClient instance.</param>
/// <param name="baseUrl">The base URL associated with this client.</param>
public FlurlClient(HttpClient httpClient, string baseUrl = null) {
/// <param name="baseUrl">Optional. The base URL associated with this client.</param>
/// <param name="settings">Optional. A pre-initialized collection of settings.</param>
/// <param name="headers">Optional. A pre-initialized collection of default request headers.</param>
public FlurlClient(HttpClient httpClient, string baseUrl = null, FlurlHttpSettings settings = null, INameValueList<string> 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<string>(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);
}
}

/// <inheritdoc />
public string BaseUrl { get; set; }

/// <inheritdoc />
public FlurlHttpSettings Settings { get; } = new();
public FlurlHttpSettings Settings { get; }

/// <inheritdoc />
public INameValueList<string> Headers { get; } = new NameValueList<string>(false); // header names are case-insensitive https://stackoverflow.com/a/5259004/62600
public INameValueList<string> Headers { get; }

/// <inheritdoc />
public HttpClient HttpClient { get; }
Expand Down Expand Up @@ -153,10 +159,7 @@ public async Task<IFlurlResponse> 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)
Expand Down
12 changes: 11 additions & 1 deletion src/Flurl.Http/FlurlRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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().
/// </summary>
public interface IFlurlRequest : IHttpSettingsContainer
public interface IFlurlRequest : ISettingsContainer, IHeadersContainer
{
/// <summary>
/// Gets or sets the IFlurlClient to use when sending the request.
Expand Down Expand Up @@ -106,6 +106,7 @@ public IFlurlClient Client {
set {
_client = value;
Settings.Parent = _client?.Settings;
SyncHeaders(_client, this);
}
}

Expand Down Expand Up @@ -142,6 +143,15 @@ public Task<IFlurlResponse> 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)
Expand Down
76 changes: 0 additions & 76 deletions src/Flurl.Http/HeaderExtensions.cs

This file was deleted.

84 changes: 84 additions & 0 deletions src/Flurl.Http/IHeadersContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System;
using System.Collections;
using System.Text;
using Flurl.Util;

namespace Flurl.Http
{
/// <summary>
/// A common interface for Flurl.Http objects that contain a collection of request headers.
/// </summary>
public interface IHeadersContainer
{
/// <summary>
/// A collection of request headers.
/// </summary>
INameValueList<string> Headers { get; }
}

/// <summary>
/// Fluent extension methods for working with HTTP request headers.
/// </summary>
public static class HeaderExtensions
{
/// <summary>
/// Sets an HTTP header associated with this request or client.
/// </summary>
/// <param name="obj">Object containing request headers.</param>
/// <param name="name">HTTP header name.</param>
/// <param name="value">HTTP header value.</param>
/// <returns>This headers container.</returns>
public static T WithHeader<T>(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;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="obj">Object containing request headers.</param>
/// <param name="headers">Names/values of HTTP headers to set. Typically an anonymous object or IDictionary.</param>
/// <param name="replaceUnderscoreWithHyphen">If true, underscores in property names will be replaced by hyphens. Default is true.</param>
/// <returns>This headers container.</returns>
public static T WithHeaders<T>(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;
}

/// <summary>
/// Sets HTTP authorization header according to Basic Authentication protocol associated with this request or client.
/// </summary>
/// <param name="obj">Object containing request headers.</param>
/// <param name="username">Username of authenticating user.</param>
/// <param name="password">Password of authenticating user.</param>
/// <returns>This headers container.</returns>
public static T WithBasicAuth<T>(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}");
}

/// <summary>
/// Sets HTTP authorization header with acquired bearer token according to OAuth 2.0 specification associated with this request or client.
/// </summary>
/// <param name="obj">Object containing request headers.</param>
/// <param name="token">The acquired bearer token to pass.</param>
/// <returns>This headers container.</returns>
public static T WithOAuthBearerToken<T>(this T obj, string token) where T : IHeadersContainer {
return obj.WithHeader("Authorization", $"Bearer {token}");
}
}
}
23 changes: 0 additions & 23 deletions src/Flurl.Http/IHttpSettingsContainer.cs

This file was deleted.

Loading

0 comments on commit 8f4fe46

Please sign in to comment.