Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flurl.Http 4.0.0-pre7 #786

Merged
merged 4 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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