Skip to content

Commit

Permalink
Merge pull request #786 from tmenier/dev
Browse files Browse the repository at this point in the history
Flurl.Http 4.0.0-pre7
  • Loading branch information
tmenier authored Dec 8, 2023
2 parents 30b629d + 4361a2d commit cc7b511
Show file tree
Hide file tree
Showing 17 changed files with 656 additions and 289 deletions.
12 changes: 10 additions & 2 deletions src/Flurl.CodeGen/Metadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,19 @@ public static IEnumerable<ExtensionMethod> GetRequestReturningExtensions(MethodA
.AddArg("seconds", "int", "Seconds to wait before the request times out.");
yield return Create("AllowHttpStatus", "Creates a new FlurlRequest and 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.")
.AddArg("pattern", "string", "Examples: \"3xx\", \"100,300,600\", \"100-299,6xx\"");
yield return Create("AllowHttpStatus", "Creates a new FlurlRequest and adds an HttpStatusCode which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown.")
.AddArg("statusCodes", "params HttpStatusCode[]", "The HttpStatusCode(s) to allow.");
yield return Create("AllowHttpStatus", "Creates a new FlurlRequest and adds one or more response status codes which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown.")
.AddArg("statusCodes", "params int[]", "One or more response status codes that, when received, will not cause an exception to be thrown.");
yield return Create("AllowAnyHttpStatus", "Creates a new FlurlRequest and configures it to allow any returned HTTP status without throwing a FlurlHttpException.");
yield return Create("WithAutoRedirect", "Creates a new FlurlRequest and configures whether redirects are automatically followed.")
.AddArg("enabled", "bool", "true if Flurl should automatically send a new request to the redirect URL, false if it should not.");

// event handler extensions
foreach (var name in new[] { "BeforeCall", "AfterCall", "OnError", "OnRedirect" }) {
yield return Create(name, $"Creates a new FlurlRequest and adds a new {name} event handler.")
.AddArg("action", "Action<FlurlCall>", $"Action to perform when the {name} event is raised.");
yield return Create(name, $"Creates a new FlurlRequest and adds a new asynchronous {name} event handler.")
.AddArg("action", "Func<FlurlCall, Task>", $"Async action to perform when the {name} event is raised.");
}
}

/// <summary>
Expand Down
7 changes: 5 additions & 2 deletions src/Flurl.Http/Configuration/FlurlClientBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Flurl.Http.Configuration
/// <summary>
/// A builder for configuring IFlurlClient instances.
/// </summary>
public interface IFlurlClientBuilder : ISettingsContainer, IHeadersContainer
public interface IFlurlClientBuilder : ISettingsContainer, IHeadersContainer, IEventHandlerContainer
{
/// <summary>
/// Configure the HttpClient wrapped by this IFlurlClient.
Expand Down Expand Up @@ -59,6 +59,9 @@ public class FlurlClientBuilder : IFlurlClientBuilder
/// <inheritdoc />
public FlurlHttpSettings Settings { get; } = new();

/// <inheritdoc />
public IList<(FlurlEventType, IFlurlEventHandler)> EventHandlers { get; } = new List<(FlurlEventType, IFlurlEventHandler)>();

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

Expand Down Expand Up @@ -123,7 +126,7 @@ public IFlurlClient Build() {
foreach (var config in _clientConfigs)
config(httpCli);

return new FlurlClient(httpCli, _baseUrl, Settings, Headers);
return new FlurlClient(httpCli, _baseUrl, Settings, Headers, EventHandlers);
}
}
}
71 changes: 0 additions & 71 deletions src/Flurl.Http/Configuration/FlurlHttpSettings.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;
using Flurl.Http.Testing;

Expand Down Expand Up @@ -89,76 +88,6 @@ public ISerializer UrlEncodedSerializer {
/// </summary>
public RedirectSettings Redirects { get; }

/// <summary>
/// Gets or sets a callback that is invoked immediately before every HTTP request is sent.
/// </summary>
public Action<FlurlCall> BeforeCall {
get => Get<Action<FlurlCall>>();
set => Set(value);
}

/// <summary>
/// Gets or sets a callback that is invoked asynchronously immediately before every HTTP request is sent.
/// </summary>
public Func<FlurlCall, Task> BeforeCallAsync {
get => Get<Func<FlurlCall, Task>>();
set => Set(value);
}

/// <summary>
/// Gets or sets a callback that is invoked immediately after every HTTP response is received.
/// </summary>
public Action<FlurlCall> AfterCall {
get => Get<Action<FlurlCall>>();
set => Set(value);
}

/// <summary>
/// Gets or sets a callback that is invoked asynchronously immediately after every HTTP response is received.
/// </summary>
public Func<FlurlCall, Task> AfterCallAsync {
get => Get<Func<FlurlCall, Task>>();
set => Set(value);
}

/// <summary>
/// Gets or 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.
/// </summary>
public Action<FlurlCall> OnError {
get => Get<Action<FlurlCall>>();
set => Set(value);
}

/// <summary>
/// Gets or 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.
/// </summary>
public Func<FlurlCall, Task> OnErrorAsync {
get => Get<Func<FlurlCall, Task>>();
set => Set(value);
}

/// <summary>
/// Gets or sets a callback that is invoked when any 3xx response with a Location header is received.
/// 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.
/// </summary>
public Action<FlurlCall> OnRedirect {
get => Get<Action<FlurlCall>>();
set => Set(value);
}

/// <summary>
/// Gets or sets a callback that is invoked asynchronously when any 3xx response with a Location header is received.
/// 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.
/// </summary>
public Func<FlurlCall, Task> OnRedirectAsync {
get => Get<Func<FlurlCall, Task>>();
set => Set(value);
}

/// <summary>
/// Resets all overridden settings to their default values. For example, on a FlurlRequest,
/// all settings are reset to FlurlClient-level settings.
Expand Down
2 changes: 1 addition & 1 deletion src/Flurl.Http/Flurl.Http.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<LangVersion>9.0</LangVersion>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageId>Flurl.Http</PackageId>
<Version>4.0.0-pre6</Version>
<Version>4.0.0-pre7</Version>
<Authors>Todd Menier</Authors>
<Description>A fluent, portable, testable HTTP client library.</Description>
<PackageProjectUrl>https://flurl.dev</PackageProjectUrl>
Expand Down
5 changes: 5 additions & 0 deletions src/Flurl.Http/FlurlCall.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ namespace Flurl.Http
/// </summary>
public class FlurlCall
{
/// <summary>
/// The IFlurlClient used to make this call.
/// </summary>
public IFlurlClient Client { get; set; }

/// <summary>
/// The IFlurlRequest associated with this call.
/// </summary>
Expand Down
75 changes: 59 additions & 16 deletions src/Flurl.Http/FlurlClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
using Flurl.Http.Configuration;
using Flurl.Http.Testing;
using Flurl.Util;
using System.Collections.Generic;
using System.Net.Http.Headers;
using System.Reflection;

namespace Flurl.Http
{
/// <summary>
/// Interface defining FlurlClient's contract (useful for mocking and DI)
/// </summary>
public interface IFlurlClient : ISettingsContainer, IHeadersContainer, IDisposable {
public interface IFlurlClient : ISettingsContainer, IHeadersContainer, IEventHandlerContainer, IDisposable {
/// <summary>
/// Gets the HttpClient that this IFlurlClient wraps.
/// </summary>
Expand Down Expand Up @@ -68,29 +71,57 @@ public FlurlClient(string baseUrl = null) : this(_defaultFactory.Value.CreateHtt
/// </summary>
/// <param name="httpClient">The instantiated HttpClient instance.</param>
/// <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) {
public FlurlClient(HttpClient httpClient, string baseUrl = null) : this(httpClient, baseUrl, null, null, null) { }

// FlurlClientBuilder gets some special privileges
internal FlurlClient(HttpClient httpClient, string baseUrl, FlurlHttpSettings settings, INameValueList<string> headers, IList<(FlurlEventType, IFlurlEventHandler)> eventHandlers) {
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
BaseUrl = baseUrl ?? httpClient.BaseAddress?.ToString();

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;

EventHandlers = eventHandlers ?? new List<(FlurlEventType, IFlurlEventHandler)>();

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))

foreach (var header in GetHeadersFromHttpClient(httpClient)) {
if (!Headers.Contains(header.Name))
Headers.Add(header);
}
}

// reflection is (relatively) expensive, so keep a cache of HttpRequestHeaders properties
// https://learn.microsoft.com/en-us/dotnet/api/system.net.http.headers.httprequestheaders?#properties
private static IDictionary<string, PropertyInfo> _reqHeaderProps =
typeof(HttpRequestHeaders).GetProperties().ToDictionary(p => p.Name.ToLower(), p => p);

private static IEnumerable<(string Name, string Value)> GetHeadersFromHttpClient(HttpClient httpClient) {
foreach (var h in httpClient.DefaultRequestHeaders) {
// MS isn't making this easy. In some cases, a header value will be split into multiple values, but when iterating the collection
// there's no way to know exactly how to piece them back together. The standard says multiple values should be comma-delimited,
// but with User-Agent they need to be space-delimited. ToString() on properties like UserAgent do this correctly though, so when spinning
// through the collection we'll try to match the header name to a property and ToString() it, otherwise we'll comma-delimit the values.
if (_reqHeaderProps.TryGetValue(h.Key.Replace("-", "").ToLower(), out var prop)) {
var val = prop.GetValue(httpClient.DefaultRequestHeaders).ToString();
yield return (h.Key, val);
}
else {
yield return (h.Key, string.Join(",", h.Value));
}
}
}

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

/// <inheritdoc />
public FlurlHttpSettings Settings { get; }

/// <inheritdoc />
public IList<(FlurlEventType, IFlurlEventHandler)> EventHandlers { get; }

/// <inheritdoc />
public INameValueList<string> Headers { get; }

Expand All @@ -117,11 +148,12 @@ public async Task<IFlurlResponse> SendAsync(IFlurlRequest request, HttpCompletio

SyncHeaders(request, reqMsg);
var call = new FlurlCall {
Client = this,
Request = request,
HttpRequestMessage = reqMsg
};

await RaiseEventAsync(settings.BeforeCall, settings.BeforeCallAsync, call).ConfigureAwait(false);
await RaiseEventAsync(FlurlEventType.BeforeCall, call).ConfigureAwait(false);

// in case URL or headers were modified in the handler above
reqMsg.RequestUri = request.Url.ToUri();
Expand Down Expand Up @@ -153,7 +185,7 @@ public async Task<IFlurlResponse> SendAsync(IFlurlRequest request, HttpCompletio
finally {
cts?.Dispose();
call.EndedUtc = DateTime.UtcNow;
await RaiseEventAsync(settings.AfterCall, settings.AfterCallAsync, call).ConfigureAwait(false);
await RaiseEventAsync(FlurlEventType.AfterCall, call).ConfigureAwait(false);
}
}

Expand Down Expand Up @@ -183,7 +215,7 @@ private async Task<IFlurlResponse> ProcessRedirectAsync(FlurlCall call, HttpComp
if (call.Redirect == null)
return null;

await RaiseEventAsync(settings.OnRedirect, settings.OnRedirectAsync, call).ConfigureAwait(false);
await RaiseEventAsync(FlurlEventType.OnRedirect, call).ConfigureAwait(false);

if (call.Redirect.Follow != true)
return null;
Expand All @@ -198,6 +230,9 @@ private async Task<IFlurlResponse> ProcessRedirectAsync(FlurlCall call, HttpComp
Settings = { Parent = settings }
};

foreach (var handler in call.Request.EventHandlers)
redir.EventHandlers.Add(handler);

if (call.Request.CookieJar != null)
redir.CookieJar = call.Request.CookieJar;

Expand Down Expand Up @@ -272,17 +307,25 @@ bool ChangeVerbToGetOn(int statusCode, HttpMethod verb) {

return redir;
}

private static Task RaiseEventAsync(Action<FlurlCall> syncHandler, Func<FlurlCall, Task> asyncHandler, FlurlCall call) {
syncHandler?.Invoke(call);
if (asyncHandler != null)
return asyncHandler(call);
return Task.FromResult(0);

internal static async Task RaiseEventAsync(FlurlEventType eventType, FlurlCall call) {
// client-level handlers first, then request-level
var handlers = call.Client.EventHandlers
.Concat(call.Request.EventHandlers)
.Where(h => h.EventType == eventType)
.Select(h => h.Handler)
.ToList();

foreach (var handler in handlers) {
// sync first, then async
handler.Handle(eventType, call);
await handler.HandleAsync(eventType, call);
}
}

internal static async Task<IFlurlResponse> HandleExceptionAsync(FlurlCall call, Exception ex, CancellationToken token) {
call.Exception = ex;
await RaiseEventAsync(call.Request.Settings.OnError, call.Request.Settings.OnErrorAsync, call).ConfigureAwait(false);
await RaiseEventAsync(FlurlEventType.OnError, call).ConfigureAwait(false);

if (call.ExceptionHandled)
return call.Response;
Expand Down
85 changes: 85 additions & 0 deletions src/Flurl.Http/FlurlEventHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System;
using System.Linq;
using System.Threading.Tasks;

namespace Flurl.Http
{
/// <summary>
/// Types of events raised by Flurl over the course of a call that can be handled via event handlers.
/// </summary>
public enum FlurlEventType
{
/// <summary>
/// Fired immediately before an HTTP request is sent.
/// </summary>
BeforeCall,

/// <summary>
/// Fired immediately after an HTTP response is received.
/// </summary>
AfterCall,

/// <summary>
/// Fired when an HTTP error response is received, just before AfterCall is fired. Error
/// responses include any status in 4xx or 5xx range by default, configurable via AllowHttpStatus.
/// You can inspect call.Exception for details, and optionally set call.ExceptionHandled to
/// true to prevent the exception from bubbling up after the handler exits.
/// </summary>
OnError,

/// <summary>
/// Fired when any 3xx response with a Location header is received, just before AfterCall is fired
/// and before the subsequent (redirected) request is sent. 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.
/// </summary>
OnRedirect
}

/// <summary>
/// Defines a handler for Flurl events such as BeforeCall, AfterCall, and OnError
/// </summary>
public interface IFlurlEventHandler
{
/// <summary>
/// Action to take when a Flurl event fires. Prefer HandleAsync if async calls need to be made.
/// </summary>
void Handle(FlurlEventType eventType, FlurlCall call);

/// <summary>
/// Asynchronous action to take when a Flurl event fires.
/// </summary>
Task HandleAsync(FlurlEventType eventType, FlurlCall call);
}

/// <summary>
/// Default implementation of IFlurlEventHandler. Typically, you should override Handle or HandleAsync.
/// Both are noops by default.
/// </summary>
public class FlurlEventHandler : IFlurlEventHandler
{
/// <summary>
/// Override to define an action to take when a Flurl event fires. Prefer HandleAsync if async calls need to be made.
/// </summary>
public virtual void Handle(FlurlEventType eventType, FlurlCall call) { }

/// <summary>
/// Override to define an asynchronous action to take when a Flurl event fires.
/// </summary>
public virtual Task HandleAsync(FlurlEventType eventType, FlurlCall call) => Task.CompletedTask;

internal class FromAction : FlurlEventHandler
{
private readonly Action<FlurlCall> _act;
public FromAction(Action<FlurlCall> act) => _act = act;
public override void Handle(FlurlEventType eventType, FlurlCall call) => _act(call);
}

internal class FromAsyncAction : FlurlEventHandler
{
private readonly Func<FlurlCall, Task> _act;
public FromAsyncAction(Func<FlurlCall, Task> act) => _act = act;
public override Task HandleAsync(FlurlEventType eventType, FlurlCall call) => _act(call);
}
}
}
Loading

0 comments on commit cc7b511

Please sign in to comment.