Skip to content

Commit

Permalink
#784 event handlers overhaul
Browse files Browse the repository at this point in the history
  • Loading branch information
Todd committed Dec 8, 2023
1 parent 759d81b commit 4def7d7
Show file tree
Hide file tree
Showing 15 changed files with 599 additions and 269 deletions.
8 changes: 8 additions & 0 deletions src/Flurl.CodeGen/Metadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ public static IEnumerable<ExtensionMethod> GetRequestReturningExtensions(MethodA
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
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
47 changes: 33 additions & 14 deletions src/Flurl.Http/FlurlClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
using Flurl.Http.Configuration;
using Flurl.Http.Testing;
using Flurl.Util;
using System.Collections.Generic;

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,16 +69,19 @@ 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))
Expand All @@ -91,6 +95,9 @@ public FlurlClient(HttpClient httpClient, string baseUrl = null, FlurlHttpSettin
/// <inheritdoc />
public FlurlHttpSettings Settings { get; }

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

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

Expand All @@ -117,11 +124,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 +161,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 +191,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 +206,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 +283,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);
}
}
}
5 changes: 4 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 : ISettingsContainer, IHeadersContainer
public interface IFlurlRequest : ISettingsContainer, IHeadersContainer, IEventHandlerContainer
{
/// <summary>
/// Gets or sets the IFlurlClient to use when sending the request.
Expand Down Expand Up @@ -100,6 +100,9 @@ internal FlurlRequest(string baseUrl, params object[] urlSegments) {
/// <inheritdoc />
public FlurlHttpSettings Settings { get; } = new();

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

/// <inheritdoc />
public IFlurlClient Client {
get => _client;
Expand Down
Loading

0 comments on commit 4def7d7

Please sign in to comment.