From 4def7d7ba01b174e78a1f25555873a6fd52881b0 Mon Sep 17 00:00:00 2001 From: Todd Date: Thu, 7 Dec 2023 21:16:27 -0600 Subject: [PATCH 1/4] #784 event handlers overhaul --- src/Flurl.CodeGen/Metadata.cs | 8 + .../Configuration/FlurlClientBuilder.cs | 7 +- .../Configuration/FlurlHttpSettings.cs | 71 ------ src/Flurl.Http/FlurlCall.cs | 5 + src/Flurl.Http/FlurlClient.cs | 47 +++- src/Flurl.Http/FlurlEventHandler.cs | 85 +++++++ src/Flurl.Http/FlurlRequest.cs | 5 +- src/Flurl.Http/GeneratedExtensions.cs | 240 ++++++++++++++++++ src/Flurl.Http/IEventHandlerContainer.cs | 81 ++++++ src/Flurl.Http/ISettingsContainer.cs | 81 +----- test/Flurl.Test/Http/EventHandlerTests.cs | 114 +++++++++ test/Flurl.Test/Http/GetTests.cs | 16 +- test/Flurl.Test/Http/RealHttpTests.cs | 24 +- test/Flurl.Test/Http/SettingsTests.cs | 80 +----- test/Flurl.Test/Http/TestingTests.cs | 4 +- 15 files changed, 599 insertions(+), 269 deletions(-) create mode 100644 src/Flurl.Http/FlurlEventHandler.cs create mode 100644 src/Flurl.Http/IEventHandlerContainer.cs create mode 100644 test/Flurl.Test/Http/EventHandlerTests.cs diff --git a/src/Flurl.CodeGen/Metadata.cs b/src/Flurl.CodeGen/Metadata.cs index 6c80e567..9b8aa135 100644 --- a/src/Flurl.CodeGen/Metadata.cs +++ b/src/Flurl.CodeGen/Metadata.cs @@ -144,6 +144,14 @@ public static IEnumerable 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", $"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", $"Async action to perform when the {name} event is raised."); + } } /// diff --git a/src/Flurl.Http/Configuration/FlurlClientBuilder.cs b/src/Flurl.Http/Configuration/FlurlClientBuilder.cs index aaced9ea..44d1a4d2 100644 --- a/src/Flurl.Http/Configuration/FlurlClientBuilder.cs +++ b/src/Flurl.Http/Configuration/FlurlClientBuilder.cs @@ -10,7 +10,7 @@ namespace Flurl.Http.Configuration /// /// A builder for configuring IFlurlClient instances. /// - public interface IFlurlClientBuilder : ISettingsContainer, IHeadersContainer + public interface IFlurlClientBuilder : ISettingsContainer, IHeadersContainer, IEventHandlerContainer { /// /// Configure the HttpClient wrapped by this IFlurlClient. @@ -59,6 +59,9 @@ public class FlurlClientBuilder : IFlurlClientBuilder /// public FlurlHttpSettings Settings { get; } = new(); + /// + public IList<(FlurlEventType, IFlurlEventHandler)> EventHandlers { get; } = new List<(FlurlEventType, IFlurlEventHandler)>(); + /// public INameValueList Headers { get; } = new NameValueList(false); // header names are case-insensitive https://stackoverflow.com/a/5259004/62600 @@ -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); } } } diff --git a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs index 760bbc4b..f068f4db 100644 --- a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs +++ b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Threading.Tasks; using System.Runtime.CompilerServices; using Flurl.Http.Testing; @@ -89,76 +88,6 @@ public ISerializer UrlEncodedSerializer { /// public RedirectSettings Redirects { get; } - /// - /// Gets or sets a callback that is invoked immediately before every HTTP request is sent. - /// - public Action BeforeCall { - get => Get>(); - set => Set(value); - } - - /// - /// Gets or sets a callback that is invoked asynchronously immediately before every HTTP request is sent. - /// - public Func BeforeCallAsync { - get => Get>(); - set => Set(value); - } - - /// - /// Gets or sets a callback that is invoked immediately after every HTTP response is received. - /// - public Action AfterCall { - get => Get>(); - set => Set(value); - } - - /// - /// Gets or sets a callback that is invoked asynchronously immediately after every HTTP response is received. - /// - public Func AfterCallAsync { - get => Get>(); - set => Set(value); - } - - /// - /// 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. - /// - public Action OnError { - get => Get>(); - set => Set(value); - } - - /// - /// 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. - /// - public Func OnErrorAsync { - get => Get>(); - set => Set(value); - } - - /// - /// 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. - /// - public Action OnRedirect { - get => Get>(); - set => Set(value); - } - - /// - /// 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. - /// - public Func OnRedirectAsync { - get => Get>(); - set => Set(value); - } - /// /// Resets all overridden settings to their default values. For example, on a FlurlRequest, /// all settings are reset to FlurlClient-level settings. diff --git a/src/Flurl.Http/FlurlCall.cs b/src/Flurl.Http/FlurlCall.cs index 7c2e75d0..b109a360 100644 --- a/src/Flurl.Http/FlurlCall.cs +++ b/src/Flurl.Http/FlurlCall.cs @@ -10,6 +10,11 @@ namespace Flurl.Http /// public class FlurlCall { + /// + /// The IFlurlClient used to make this call. + /// + public IFlurlClient Client { get; set; } + /// /// The IFlurlRequest associated with this call. /// diff --git a/src/Flurl.Http/FlurlClient.cs b/src/Flurl.Http/FlurlClient.cs index 89e56e83..b23dd0a2 100644 --- a/src/Flurl.Http/FlurlClient.cs +++ b/src/Flurl.Http/FlurlClient.cs @@ -6,13 +6,14 @@ using Flurl.Http.Configuration; using Flurl.Http.Testing; using Flurl.Util; +using System.Collections.Generic; namespace Flurl.Http { /// /// Interface defining FlurlClient's contract (useful for mocking and DI) /// - public interface IFlurlClient : ISettingsContainer, IHeadersContainer, IDisposable { + public interface IFlurlClient : ISettingsContainer, IHeadersContainer, IEventHandlerContainer, IDisposable { /// /// Gets the HttpClient that this IFlurlClient wraps. /// @@ -68,9 +69,10 @@ public FlurlClient(string baseUrl = null) : this(_defaultFactory.Value.CreateHtt /// /// The instantiated HttpClient instance. /// 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) { + 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 headers, IList<(FlurlEventType, IFlurlEventHandler)> eventHandlers) { HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); BaseUrl = baseUrl ?? httpClient.BaseAddress?.ToString(); @@ -78,6 +80,8 @@ public FlurlClient(HttpClient httpClient, string baseUrl = null, FlurlHttpSettin // 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(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)) @@ -91,6 +95,9 @@ public FlurlClient(HttpClient httpClient, string baseUrl = null, FlurlHttpSettin /// public FlurlHttpSettings Settings { get; } + /// + public IList<(FlurlEventType, IFlurlEventHandler)> EventHandlers { get; } + /// public INameValueList Headers { get; } @@ -117,11 +124,12 @@ public async Task 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(); @@ -153,7 +161,7 @@ public async Task 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); } } @@ -183,7 +191,7 @@ private async Task 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; @@ -198,6 +206,9 @@ private async Task 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; @@ -272,17 +283,25 @@ bool ChangeVerbToGetOn(int statusCode, HttpMethod verb) { return redir; } - - private static Task RaiseEventAsync(Action syncHandler, Func 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 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; diff --git a/src/Flurl.Http/FlurlEventHandler.cs b/src/Flurl.Http/FlurlEventHandler.cs new file mode 100644 index 00000000..d9137ea8 --- /dev/null +++ b/src/Flurl.Http/FlurlEventHandler.cs @@ -0,0 +1,85 @@ +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Flurl.Http +{ + /// + /// Types of events raised by Flurl over the course of a call that can be handled via event handlers. + /// + public enum FlurlEventType + { + /// + /// Fired immediately before an HTTP request is sent. + /// + BeforeCall, + + /// + /// Fired immediately after an HTTP response is received. + /// + AfterCall, + + /// + /// 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. + /// + OnError, + + /// + /// 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. + /// + OnRedirect + } + + /// + /// Defines a handler for Flurl events such as BeforeCall, AfterCall, and OnError + /// + public interface IFlurlEventHandler + { + /// + /// Action to take when a Flurl event fires. Prefer HandleAsync if async calls need to be made. + /// + void Handle(FlurlEventType eventType, FlurlCall call); + + /// + /// Asynchronous action to take when a Flurl event fires. + /// + Task HandleAsync(FlurlEventType eventType, FlurlCall call); + } + + /// + /// Default implementation of IFlurlEventHandler. Typically, you should override Handle or HandleAsync. + /// Both are noops by default. + /// + public class FlurlEventHandler : IFlurlEventHandler + { + /// + /// Override to define an action to take when a Flurl event fires. Prefer HandleAsync if async calls need to be made. + /// + public virtual void Handle(FlurlEventType eventType, FlurlCall call) { } + + /// + /// Override to define an asynchronous action to take when a Flurl event fires. + /// + public virtual Task HandleAsync(FlurlEventType eventType, FlurlCall call) => Task.CompletedTask; + + internal class FromAction : FlurlEventHandler + { + private readonly Action _act; + public FromAction(Action act) => _act = act; + public override void Handle(FlurlEventType eventType, FlurlCall call) => _act(call); + } + + internal class FromAsyncAction : FlurlEventHandler + { + private readonly Func _act; + public FromAsyncAction(Func act) => _act = act; + public override Task HandleAsync(FlurlEventType eventType, FlurlCall call) => _act(call); + } + } +} diff --git a/src/Flurl.Http/FlurlRequest.cs b/src/Flurl.Http/FlurlRequest.cs index ce11b73a..9a20926a 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 : ISettingsContainer, IHeadersContainer + public interface IFlurlRequest : ISettingsContainer, IHeadersContainer, IEventHandlerContainer { /// /// Gets or sets the IFlurlClient to use when sending the request. @@ -100,6 +100,9 @@ internal FlurlRequest(string baseUrl, params object[] urlSegments) { /// public FlurlHttpSettings Settings { get; } = new(); + /// + public IList<(FlurlEventType, IFlurlEventHandler)> EventHandlers { get; } = new List<(FlurlEventType, IFlurlEventHandler)>(); + /// public IFlurlClient Client { get => _client; diff --git a/src/Flurl.Http/GeneratedExtensions.cs b/src/Flurl.Http/GeneratedExtensions.cs index 5b10e939..e197a350 100644 --- a/src/Flurl.Http/GeneratedExtensions.cs +++ b/src/Flurl.Http/GeneratedExtensions.cs @@ -711,6 +711,86 @@ public static IFlurlRequest WithAutoRedirect(this Url url, bool enabled) { return new FlurlRequest(url).WithAutoRedirect(enabled); } + /// + /// Creates a new FlurlRequest and adds a new BeforeCall event handler. + /// + /// This Flurl.Url. + /// Action to perform when the BeforeCall event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest BeforeCall(this Url url, Action action) { + return new FlurlRequest(url).BeforeCall(action); + } + + /// + /// Creates a new FlurlRequest and adds a new asynchronous BeforeCall event handler. + /// + /// This Flurl.Url. + /// Async action to perform when the BeforeCall event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest BeforeCall(this Url url, Func action) { + return new FlurlRequest(url).BeforeCall(action); + } + + /// + /// Creates a new FlurlRequest and adds a new AfterCall event handler. + /// + /// This Flurl.Url. + /// Action to perform when the AfterCall event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest AfterCall(this Url url, Action action) { + return new FlurlRequest(url).AfterCall(action); + } + + /// + /// Creates a new FlurlRequest and adds a new asynchronous AfterCall event handler. + /// + /// This Flurl.Url. + /// Async action to perform when the AfterCall event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest AfterCall(this Url url, Func action) { + return new FlurlRequest(url).AfterCall(action); + } + + /// + /// Creates a new FlurlRequest and adds a new OnError event handler. + /// + /// This Flurl.Url. + /// Action to perform when the OnError event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest OnError(this Url url, Action action) { + return new FlurlRequest(url).OnError(action); + } + + /// + /// Creates a new FlurlRequest and adds a new asynchronous OnError event handler. + /// + /// This Flurl.Url. + /// Async action to perform when the OnError event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest OnError(this Url url, Func action) { + return new FlurlRequest(url).OnError(action); + } + + /// + /// Creates a new FlurlRequest and adds a new OnRedirect event handler. + /// + /// This Flurl.Url. + /// Action to perform when the OnRedirect event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest OnRedirect(this Url url, Action action) { + return new FlurlRequest(url).OnRedirect(action); + } + + /// + /// Creates a new FlurlRequest and adds a new asynchronous OnRedirect event handler. + /// + /// This Flurl.Url. + /// Async action to perform when the OnRedirect event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest OnRedirect(this Url url, Func action) { + return new FlurlRequest(url).OnRedirect(action); + } + /// /// Creates a FlurlRequest and sends an asynchronous request. /// @@ -1150,6 +1230,86 @@ public static IFlurlRequest WithAutoRedirect(this string url, bool enabled) { return new FlurlRequest(url).WithAutoRedirect(enabled); } + /// + /// Creates a new FlurlRequest and adds a new BeforeCall event handler. + /// + /// This URL. + /// Action to perform when the BeforeCall event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest BeforeCall(this string url, Action action) { + return new FlurlRequest(url).BeforeCall(action); + } + + /// + /// Creates a new FlurlRequest and adds a new asynchronous BeforeCall event handler. + /// + /// This URL. + /// Async action to perform when the BeforeCall event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest BeforeCall(this string url, Func action) { + return new FlurlRequest(url).BeforeCall(action); + } + + /// + /// Creates a new FlurlRequest and adds a new AfterCall event handler. + /// + /// This URL. + /// Action to perform when the AfterCall event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest AfterCall(this string url, Action action) { + return new FlurlRequest(url).AfterCall(action); + } + + /// + /// Creates a new FlurlRequest and adds a new asynchronous AfterCall event handler. + /// + /// This URL. + /// Async action to perform when the AfterCall event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest AfterCall(this string url, Func action) { + return new FlurlRequest(url).AfterCall(action); + } + + /// + /// Creates a new FlurlRequest and adds a new OnError event handler. + /// + /// This URL. + /// Action to perform when the OnError event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest OnError(this string url, Action action) { + return new FlurlRequest(url).OnError(action); + } + + /// + /// Creates a new FlurlRequest and adds a new asynchronous OnError event handler. + /// + /// This URL. + /// Async action to perform when the OnError event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest OnError(this string url, Func action) { + return new FlurlRequest(url).OnError(action); + } + + /// + /// Creates a new FlurlRequest and adds a new OnRedirect event handler. + /// + /// This URL. + /// Action to perform when the OnRedirect event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest OnRedirect(this string url, Action action) { + return new FlurlRequest(url).OnRedirect(action); + } + + /// + /// Creates a new FlurlRequest and adds a new asynchronous OnRedirect event handler. + /// + /// This URL. + /// Async action to perform when the OnRedirect event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest OnRedirect(this string url, Func action) { + return new FlurlRequest(url).OnRedirect(action); + } + /// /// Creates a FlurlRequest and sends an asynchronous request. /// @@ -1589,5 +1749,85 @@ public static IFlurlRequest WithAutoRedirect(this Uri uri, bool enabled) { return new FlurlRequest(uri).WithAutoRedirect(enabled); } + /// + /// Creates a new FlurlRequest and adds a new BeforeCall event handler. + /// + /// This System.Uri. + /// Action to perform when the BeforeCall event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest BeforeCall(this Uri uri, Action action) { + return new FlurlRequest(uri).BeforeCall(action); + } + + /// + /// Creates a new FlurlRequest and adds a new asynchronous BeforeCall event handler. + /// + /// This System.Uri. + /// Async action to perform when the BeforeCall event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest BeforeCall(this Uri uri, Func action) { + return new FlurlRequest(uri).BeforeCall(action); + } + + /// + /// Creates a new FlurlRequest and adds a new AfterCall event handler. + /// + /// This System.Uri. + /// Action to perform when the AfterCall event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest AfterCall(this Uri uri, Action action) { + return new FlurlRequest(uri).AfterCall(action); + } + + /// + /// Creates a new FlurlRequest and adds a new asynchronous AfterCall event handler. + /// + /// This System.Uri. + /// Async action to perform when the AfterCall event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest AfterCall(this Uri uri, Func action) { + return new FlurlRequest(uri).AfterCall(action); + } + + /// + /// Creates a new FlurlRequest and adds a new OnError event handler. + /// + /// This System.Uri. + /// Action to perform when the OnError event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest OnError(this Uri uri, Action action) { + return new FlurlRequest(uri).OnError(action); + } + + /// + /// Creates a new FlurlRequest and adds a new asynchronous OnError event handler. + /// + /// This System.Uri. + /// Async action to perform when the OnError event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest OnError(this Uri uri, Func action) { + return new FlurlRequest(uri).OnError(action); + } + + /// + /// Creates a new FlurlRequest and adds a new OnRedirect event handler. + /// + /// This System.Uri. + /// Action to perform when the OnRedirect event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest OnRedirect(this Uri uri, Action action) { + return new FlurlRequest(uri).OnRedirect(action); + } + + /// + /// Creates a new FlurlRequest and adds a new asynchronous OnRedirect event handler. + /// + /// This System.Uri. + /// Async action to perform when the OnRedirect event is raised. + /// A new IFlurlRequest. + public static IFlurlRequest OnRedirect(this Uri uri, Func action) { + return new FlurlRequest(uri).OnRedirect(action); + } + } } diff --git a/src/Flurl.Http/IEventHandlerContainer.cs b/src/Flurl.Http/IEventHandlerContainer.cs new file mode 100644 index 00000000..879fdaf6 --- /dev/null +++ b/src/Flurl.Http/IEventHandlerContainer.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Flurl.Http +{ + /// + /// A common interface for Flurl.Http objects that contain event handlers. + /// + public interface IEventHandlerContainer + { + /// + /// A collection of Flurl event handlers. + /// + IList<(FlurlEventType EventType, IFlurlEventHandler Handler)> EventHandlers { get; } + } + + /// + /// Fluent extension methods for tweaking FlurlHttpSettings + /// + public static class EventHandlerContainerExtensions + { + /// + /// Adds an event handler that is invoked when a BeforeCall event is fired. + /// + /// This event handler container. + public static T BeforeCall(this T obj, Action act) where T : IEventHandlerContainer => AddHandler(obj, FlurlEventType.BeforeCall, act); + + /// + /// Adds an asynchronous event handler that is invoked when a BeforeCall event is fired. + /// + /// This event handler container. + public static T BeforeCall(this T obj, Func act) where T : IEventHandlerContainer => AddHandler(obj, FlurlEventType.BeforeCall, act); + + /// + /// Adds an event handler that is invoked when an AfterCall event is fired. + /// + /// This event handler container. + public static T AfterCall(this T obj, Action act) where T : IEventHandlerContainer => AddHandler(obj, FlurlEventType.AfterCall, act); + + /// + /// Adds an asynchronous event handler that is invoked when an AfterCall event is fired. + /// + /// This event handler container. + public static T AfterCall(this T obj, Func act) where T : IEventHandlerContainer => AddHandler(obj, FlurlEventType.AfterCall, act); + + /// + /// Adds an event handler that is invoked when an OnError event is fired. + /// + /// This event handler container. + public static T OnError(this T obj, Action act) where T : IEventHandlerContainer => AddHandler(obj, FlurlEventType.OnError, act); + + /// + /// Adds an asynchronous event handler that is invoked when an OnError event is fired. + /// + /// This event handler container. + public static T OnError(this T obj, Func act) where T : IEventHandlerContainer => AddHandler(obj, FlurlEventType.OnError, act); + + /// + /// Adds an event handler that is invoked when an OnRedirect event is fired. + /// + /// This event handler container. + public static T OnRedirect(this T obj, Action act) where T : IEventHandlerContainer => AddHandler(obj, FlurlEventType.OnRedirect, act); + + /// + /// Adds an asynchronous event handler that is invoked when an OnRedirect event is fired. + /// + /// This event handler container. + public static T OnRedirect(this T obj, Func act) where T : IEventHandlerContainer => AddHandler(obj, FlurlEventType.OnRedirect, act); + + private static T AddHandler(T obj, FlurlEventType eventType, Action act) where T : IEventHandlerContainer { + obj.EventHandlers.Add((eventType, new FlurlEventHandler.FromAction(act))); + return obj; + } + + private static T AddHandler(T obj, FlurlEventType eventType, Func act) where T : IEventHandlerContainer { + obj.EventHandlers.Add((eventType, new FlurlEventHandler.FromAsyncAction(act))); + return obj; + } + } +} diff --git a/src/Flurl.Http/ISettingsContainer.cs b/src/Flurl.Http/ISettingsContainer.cs index 2c8a6d0b..4f8f8254 100644 --- a/src/Flurl.Http/ISettingsContainer.cs +++ b/src/Flurl.Http/ISettingsContainer.cs @@ -1,13 +1,12 @@ using System; using System.Linq; using System.Net; -using System.Threading.Tasks; using Flurl.Http.Configuration; namespace Flurl.Http { /// - /// A common interface for Flurl.Http objects that contain a collection of request settings. + /// A common interface for Flurl.Http objects that are configurable via a Settings property. /// public interface ISettingsContainer { @@ -103,83 +102,5 @@ public static T WithAutoRedirect(this T obj, bool enabled) where T : ISetting obj.Settings.Redirects.Enabled = enabled; return obj; } - - /// - /// Sets a callback that is invoked immediately before every HTTP request is sent. - /// - /// This settings container. - public static T BeforeCall(this T obj, Action act) where T : ISettingsContainer { - obj.Settings.BeforeCall = act; - return obj; - } - - /// - /// Sets a callback that is invoked asynchronously immediately before every HTTP request is sent. - /// - /// This settings container. - public static T BeforeCall(this T obj, Func act) where T : ISettingsContainer { - obj.Settings.BeforeCallAsync = act; - return obj; - } - - /// - /// Sets a callback that is invoked immediately after every HTTP response is received. - /// - /// This settings container. - public static T AfterCall(this T obj, Action act) where T : ISettingsContainer { - obj.Settings.AfterCall = act; - return obj; - } - - /// - /// Sets a callback that is invoked asynchronously immediately after every HTTP response is received. - /// - /// This settings container. - public static T AfterCall(this T obj, Func act) where T : ISettingsContainer { - obj.Settings.AfterCallAsync = act; - return obj; - } - - /// - /// 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. - /// - /// This settings container. - public static T OnError(this T obj, Action act) where T : ISettingsContainer { - obj.Settings.OnError = act; - return obj; - } - - /// - /// 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. - /// - /// This settings container. - public static T OnError(this T obj, Func act) where T : ISettingsContainer { - obj.Settings.OnErrorAsync = act; - return obj; - } - - /// - /// 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. - /// - /// This settings container. - public static T OnRedirect(this T obj, Action act) where T : ISettingsContainer { - obj.Settings.OnRedirect = act; - return obj; - } - - /// - /// 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. - /// - /// This settings container. - public static T OnRedirect(this T obj, Func act) where T : ISettingsContainer { - obj.Settings.OnRedirectAsync = act; - return obj; - } } } \ No newline at end of file diff --git a/test/Flurl.Test/Http/EventHandlerTests.cs b/test/Flurl.Test/Http/EventHandlerTests.cs new file mode 100644 index 00000000..e05fb201 --- /dev/null +++ b/test/Flurl.Test/Http/EventHandlerTests.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Flurl.Http; +using Flurl.Http.Configuration; +using Flurl.Http.Testing; +using NUnit.Framework; + +namespace Flurl.Test.Http +{ + /// + /// An EventHandlers collection is available on IFlurlRequest, IFlurlClient, and IFlurlClientBuilder. + /// This abstract class allows the same tests to be run against all 3. + /// + public abstract class EventHandlerTestsBase where T : IEventHandlerContainer + { + protected abstract T CreateContainer(); + protected abstract IFlurlRequest GetRequest(T container); + + [Test] + public async Task can_set_pre_callback() { + var callbackCalled = false; + using var test = new HttpTest(); + + test.RespondWith("ok"); + var c = CreateContainer().BeforeCall(call => { + Assert.Null(call.Response); // verifies that callback is running before HTTP call is made + callbackCalled = true; + }); + Assert.IsFalse(callbackCalled); + await GetRequest(c).GetAsync(); + Assert.IsTrue(callbackCalled); + } + + [Test] + public async Task can_set_post_callback() { + var callbackCalled = false; + using var test = new HttpTest(); + + test.RespondWith("ok"); + var c = CreateContainer().AfterCall(call => { + Assert.NotNull(call.Response); // verifies that callback is running after HTTP call is made + callbackCalled = true; + }); + Assert.IsFalse(callbackCalled); + await GetRequest(c).GetAsync(); + Assert.IsTrue(callbackCalled); + } + + [TestCase(true)] + [TestCase(false)] + public async Task can_set_error_callback(bool markExceptionHandled) { + var callbackCalled = false; + using var test = new HttpTest(); + + test.RespondWith("server error", 500); + var c = CreateContainer().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(c).GetAsync(); + Assert.IsTrue(callbackCalled, "OnError was never called"); + Assert.IsTrue(markExceptionHandled, "ExceptionHandled was marked false in callback, but exception was not propagated."); + } + catch (FlurlHttpException) { + Assert.IsTrue(callbackCalled, "OnError was never called"); + Assert.IsFalse(markExceptionHandled, "ExceptionHandled was marked true in callback, but exception was propagated."); + } + } + + [Test] + public async Task can_disable_exception_behavior() { + using var test = new HttpTest(); + + var c = CreateContainer().OnError(call => { + call.ExceptionHandled = true; + }); + test.RespondWith("server error", 500); + try { + var result = await GetRequest(c).GetAsync(); + Assert.AreEqual(500, result.StatusCode); + } + catch (FlurlHttpException) { + Assert.Fail("Flurl should not have thrown exception."); + } + } + } + + [TestFixture] + public class RequestEventHandlersTests : EventHandlerTestsBase + { + protected override IFlurlRequest CreateContainer() => new FlurlRequest("http://api.com"); + protected override IFlurlRequest GetRequest(IFlurlRequest req) => req; + } + + [TestFixture] + public class ClientEventHandlersTests : EventHandlerTestsBase + { + protected override IFlurlClient CreateContainer() => new FlurlClient(); + protected override IFlurlRequest GetRequest(IFlurlClient cli) => cli.Request("http://api.com"); + } + + [TestFixture] + public class ClientBuilderEventHandlersTests : EventHandlerTestsBase + { + protected override IFlurlClientBuilder CreateContainer() => new FlurlClientBuilder(); + protected override IFlurlRequest GetRequest(IFlurlClientBuilder builder) => builder.Build().Request("http://api.com"); + } +} \ No newline at end of file diff --git a/test/Flurl.Test/Http/GetTests.cs b/test/Flurl.Test/Http/GetTests.cs index 0a3cf956..0d9097bb 100644 --- a/test/Flurl.Test/Http/GetTests.cs +++ b/test/Flurl.Test/Http/GetTests.cs @@ -105,14 +105,14 @@ await ex.GetResponseJsonAsync() : } } - [Test] - public async Task can_get_null_json_when_timeout_and_exception_handled() { - HttpTest.SimulateTimeout(); - var data = await "http://api.com" - .WithSettings(c => c.OnError = call => call.ExceptionHandled = true) - .GetJsonAsync(); - Assert.IsNull(data); - } + [Test] + public async Task can_get_null_json_when_timeout_and_exception_handled() { + HttpTest.SimulateTimeout(); + var data = await "http://api.com" + .OnError(call => call.ExceptionHandled = true) + .GetJsonAsync(); + Assert.IsNull(data); + } // https://github.com/tmenier/Flurl/pull/76 // quotes around charset value is technically legal but there's a bug in .NET we want to avoid: https://github.com/dotnet/corefx/issues/5014 diff --git a/test/Flurl.Test/Http/RealHttpTests.cs b/test/Flurl.Test/Http/RealHttpTests.cs index 8466f391..8bfca19c 100644 --- a/test/Flurl.Test/Http/RealHttpTests.cs +++ b/test/Flurl.Test/Http/RealHttpTests.cs @@ -134,12 +134,12 @@ public async Task can_handle_http_error() { var handlerCalled = false; try { - await "https://httpbin.org/status/500".WithSettings(c => { - c.OnError = call => { + await "https://httpbin.org/status/500" + .OnError(call => { call.ExceptionHandled = true; handlerCalled = true; - }; - }).GetAsync(); + }) + .GetAsync(); Assert.IsTrue(handlerCalled, "error handler should have been called."); } catch (FlurlHttpException) { @@ -152,12 +152,12 @@ public async Task can_handle_parsing_error() { Exception ex = null; try { - await "http://httpbin.org/image/jpeg".WithSettings(c => { - c.OnError = call => { + await "http://httpbin.org/image/jpeg" + .OnError(call => { ex = call.Exception; call.ExceptionHandled = true; - }; - }).GetJsonAsync(); + }) + .GetJsonAsync(); Assert.IsNotNull(ex, "error handler should have been called."); Assert.IsInstanceOf(ex); } @@ -293,10 +293,10 @@ public async Task does_not_create_empty_content_for_forwarding_content_header() // Flurl was auto-creating an empty HttpContent object in order to forward content-level headers, // and on .NET Framework a GET with a non-null HttpContent throws an exceptions (#583) var calls = new List(); - var resp = await "http://httpbingo.org/redirect-to?url=http%3A%2F%2Fexample.com%2F".WithSettings(c => { - c.Redirects.ForwardHeaders = true; - c.BeforeCall = call => calls.Add(call); - }).PostUrlEncodedAsync("test=test"); + var resp = await "http://httpbingo.org/redirect-to?url=http%3A%2F%2Fexample.com%2F" + .WithSettings(c => c.Redirects.ForwardHeaders = true) + .BeforeCall(call => calls.Add(call)) + .PostUrlEncodedAsync("test=test"); Assert.AreEqual(2, calls.Count); Assert.AreEqual(HttpMethod.Post, calls[0].Request.Verb); diff --git a/test/Flurl.Test/Http/SettingsTests.cs b/test/Flurl.Test/Http/SettingsTests.cs index 9ccbe4da..c263bdb3 100644 --- a/test/Flurl.Test/Http/SettingsTests.cs +++ b/test/Flurl.Test/Http/SettingsTests.cs @@ -10,7 +10,7 @@ namespace Flurl.Test.Http { /// - /// A Settings collection is available on IFlurlRequest, IFlurlClient, IFlurlBuilder, and HttpTest. + /// A Settings collection is available on IFlurlRequest, IFlurlClient, IFlurlClientBuilder, and HttpTest. /// This abstract class allows the same tests to be run against all 4. /// public abstract class SettingsTestsBase where T : ISettingsContainer @@ -58,95 +58,18 @@ public async Task can_allow_non_success_status() { } } - [Test] - public async Task can_set_pre_callback() { - var callbackCalled = false; - using var test = new HttpTest(); - - test.RespondWith("ok"); - 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(c).GetAsync(); - Assert.IsTrue(callbackCalled); - } - - [Test] - public async Task can_set_post_callback() { - var callbackCalled = false; - using var test = new HttpTest(); - - test.RespondWith("ok"); - 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(c).GetAsync(); - Assert.IsTrue(callbackCalled); - } - - [TestCase(true)] - [TestCase(false)] - public async Task can_set_error_callback(bool markExceptionHandled) { - var callbackCalled = false; - using var test = new HttpTest(); - - test.RespondWith("server error", 500); - 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(c).GetAsync(); - Assert.IsTrue(callbackCalled, "OnError was never called"); - Assert.IsTrue(markExceptionHandled, "ExceptionHandled was marked false in callback, but exception was not propagated."); - } - catch (FlurlHttpException) { - Assert.IsTrue(callbackCalled, "OnError was never called"); - Assert.IsFalse(markExceptionHandled, "ExceptionHandled was marked true in callback, but exception was propagated."); - } - } - - [Test] - public async Task can_disable_exception_behavior() { - using var test = new HttpTest(); - - var c = CreateContainer(); - c.Settings.OnError = call => { - call.ExceptionHandled = true; - }; - test.RespondWith("server error", 500); - try { - var result = await GetRequest(c).GetAsync(); - Assert.AreEqual(500, result.StatusCode); - } - catch (FlurlHttpException) { - Assert.Fail("Flurl should not have thrown exception."); - } - } - [Test] public void can_reset_defaults() { var c = CreateContainer(); c.Settings.JsonSerializer = null; c.Settings.Redirects.Enabled = false; - c.Settings.BeforeCall = (call) => Console.WriteLine("Before!"); c.Settings.Redirects.MaxAutoRedirects = 5; var req = GetRequest(c); Assert.IsNull(req.Settings.JsonSerializer); Assert.IsFalse(req.Settings.Redirects.Enabled); - Assert.IsNotNull(req.Settings.BeforeCall); Assert.AreEqual(5, req.Settings.Redirects.MaxAutoRedirects); c.Settings.ResetDefaults(); @@ -154,7 +77,6 @@ public void can_reset_defaults() { 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); } diff --git a/test/Flurl.Test/Http/TestingTests.cs b/test/Flurl.Test/Http/TestingTests.cs index a1f40f29..79ecc652 100644 --- a/test/Flurl.Test/Http/TestingTests.cs +++ b/test/Flurl.Test/Http/TestingTests.cs @@ -394,7 +394,7 @@ public async Task can_simulate_timeout_with_exception_handled() { var exceptionCaught = false; var resp = await "http://api.com" - .WithSettings(c => c.OnError = call => { + .OnError(call => { exceptionCaught = true; var ex = call.Exception as TaskCanceledException; Assert.NotNull(ex); @@ -503,7 +503,7 @@ public async Task can_use_response_queue_in_parallel() { // virtually guaranteed without thread-safe collections backing ResponseQueue and CallLog, // but without making the test unbearably slow. var cli = new FlurlClient("http://api.com"); - cli.Settings.BeforeCallAsync = call => Task.Delay(200); + cli.BeforeCall(call => Task.Delay(200)); for (var i = 0; i < 5; i++) { using (var test = new HttpTest()) { From d9924c4012de89e07fa8fb15edebf591acb1ca44 Mon Sep 17 00:00:00 2001 From: Todd Date: Thu, 7 Dec 2023 21:33:08 -0600 Subject: [PATCH 2/4] #785 AllowHttpStatus should take int[] --- src/Flurl.CodeGen/Metadata.cs | 4 ++-- src/Flurl.Http/GeneratedExtensions.cs | 18 +++++++++--------- src/Flurl.Http/ISettingsContainer.cs | 8 ++++---- test/Flurl.Test/Http/SettingsTests.cs | 4 ++-- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Flurl.CodeGen/Metadata.cs b/src/Flurl.CodeGen/Metadata.cs index 9b8aa135..0bf9f392 100644 --- a/src/Flurl.CodeGen/Metadata.cs +++ b/src/Flurl.CodeGen/Metadata.cs @@ -139,8 +139,8 @@ public static IEnumerable 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."); diff --git a/src/Flurl.Http/GeneratedExtensions.cs b/src/Flurl.Http/GeneratedExtensions.cs index e197a350..397e2bd2 100644 --- a/src/Flurl.Http/GeneratedExtensions.cs +++ b/src/Flurl.Http/GeneratedExtensions.cs @@ -683,12 +683,12 @@ public static IFlurlRequest AllowHttpStatus(this Url url, string pattern) { } /// - /// Creates a new FlurlRequest and adds an HttpStatusCode which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown. + /// 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. /// /// This Flurl.Url. - /// The HttpStatusCode(s) to allow. + /// One or more response status codes that, when received, will not cause an exception to be thrown. /// A new IFlurlRequest. - public static IFlurlRequest AllowHttpStatus(this Url url, params HttpStatusCode[] statusCodes) { + public static IFlurlRequest AllowHttpStatus(this Url url, params int[] statusCodes) { return new FlurlRequest(url).AllowHttpStatus(statusCodes); } @@ -1202,12 +1202,12 @@ public static IFlurlRequest AllowHttpStatus(this string url, string pattern) { } /// - /// Creates a new FlurlRequest and adds an HttpStatusCode which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown. + /// 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. /// /// This URL. - /// The HttpStatusCode(s) to allow. + /// One or more response status codes that, when received, will not cause an exception to be thrown. /// A new IFlurlRequest. - public static IFlurlRequest AllowHttpStatus(this string url, params HttpStatusCode[] statusCodes) { + public static IFlurlRequest AllowHttpStatus(this string url, params int[] statusCodes) { return new FlurlRequest(url).AllowHttpStatus(statusCodes); } @@ -1721,12 +1721,12 @@ public static IFlurlRequest AllowHttpStatus(this Uri uri, string pattern) { } /// - /// Creates a new FlurlRequest and adds an HttpStatusCode which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown. + /// 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. /// /// This System.Uri. - /// The HttpStatusCode(s) to allow. + /// One or more response status codes that, when received, will not cause an exception to be thrown. /// A new IFlurlRequest. - public static IFlurlRequest AllowHttpStatus(this Uri uri, params HttpStatusCode[] statusCodes) { + public static IFlurlRequest AllowHttpStatus(this Uri uri, params int[] statusCodes) { return new FlurlRequest(uri).AllowHttpStatus(statusCodes); } diff --git a/src/Flurl.Http/ISettingsContainer.cs b/src/Flurl.Http/ISettingsContainer.cs index 4f8f8254..698fd87f 100644 --- a/src/Flurl.Http/ISettingsContainer.cs +++ b/src/Flurl.Http/ISettingsContainer.cs @@ -72,13 +72,13 @@ public static T AllowHttpStatus(this T obj, string pattern) where T : ISettin } /// - /// Adds an which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown. + /// Adds one or more response status codes which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown. /// /// Object containing settings. - /// Examples: HttpStatusCode.NotFound + /// One or more response status codes that, when received, will not cause an exception to be thrown. /// 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)); + public static T AllowHttpStatus(this T obj, params int[] statusCodes) where T : ISettingsContainer { + var pattern = string.Join(",", statusCodes); return AllowHttpStatus(obj, pattern); } diff --git a/test/Flurl.Test/Http/SettingsTests.cs b/test/Flurl.Test/Http/SettingsTests.cs index c263bdb3..ee5763c7 100644 --- a/test/Flurl.Test/Http/SettingsTests.cs +++ b/test/Flurl.Test/Http/SettingsTests.cs @@ -113,7 +113,7 @@ public void can_set_timeout_in_seconds() { 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); + var c = CreateContainer().AllowHttpStatus(409, 404); await GetRequest(c).DeleteAsync(); // no exception = pass } @@ -121,7 +121,7 @@ public async Task can_allow_specific_http_status() { 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); + var c = CreateContainer().AllowHttpStatus(409, 404); await GetRequest(c).GetAsync(); // no exception = pass } From 439afe952275d4d13ecf1483d415d83c19477937 Mon Sep 17 00:00:00 2001 From: Todd Date: Thu, 7 Dec 2023 23:25:55 -0600 Subject: [PATCH 3/4] #778 DefautRequestHeaders sync bug --- src/Flurl.Http/FlurlClient.cs | 28 ++++++++++++++++++++++++++-- test/Flurl.Test/Http/HeadersTests.cs | 13 +++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/Flurl.Http/FlurlClient.cs b/src/Flurl.Http/FlurlClient.cs index b23dd0a2..474d8568 100644 --- a/src/Flurl.Http/FlurlClient.cs +++ b/src/Flurl.Http/FlurlClient.cs @@ -7,6 +7,8 @@ using Flurl.Http.Testing; using Flurl.Util; using System.Collections.Generic; +using System.Net.Http.Headers; +using System.Reflection; namespace Flurl.Http { @@ -83,12 +85,34 @@ internal FlurlClient(HttpClient httpClient, string baseUrl, FlurlHttpSettings se EventHandlers = eventHandlers ?? new List<(FlurlEventType, IFlurlEventHandler)>(); 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)) + + 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 _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)); + } + } + } + /// public string BaseUrl { get; set; } diff --git a/test/Flurl.Test/Http/HeadersTests.cs b/test/Flurl.Test/Http/HeadersTests.cs index 684ddd40..6795e174 100644 --- a/test/Flurl.Test/Http/HeadersTests.cs +++ b/test/Flurl.Test/Http/HeadersTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Threading.Tasks; using Flurl.Http; using Flurl.Http.Configuration; @@ -147,6 +148,18 @@ public class ClientHeadersTests : HeadersTestsBase { protected override IFlurlClient CreateContainer() => new FlurlClient(); protected override IFlurlRequest GetRequest(IFlurlClient cli) => cli.Request("http://api.com"); + + [Test] + public void can_copy_multi_value_header_from_HttpClient() { + var userAgent = "Mozilla/5.0 (X11; Linux i686; rv:109.0) Gecko/20100101 Firefox/120.0"; + var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add("user-agent", userAgent); + httpClient.DefaultRequestHeaders.TryAddWithoutValidation("foo", new[] {"a", "b", "c"}); + var flurlClient = new FlurlClient(httpClient); + + Assert.AreEqual(userAgent, flurlClient.Headers[0].Value); + Assert.AreEqual("a,b,c", flurlClient.Headers[1].Value); + } } [TestFixture] From 4361a2d9fd7c082bc6a8585e7508bfdea2f93057 Mon Sep 17 00:00:00 2001 From: Todd Date: Thu, 7 Dec 2023 23:29:03 -0600 Subject: [PATCH 4/4] Flurl.Http pre version bump --- src/Flurl.Http/Flurl.Http.csproj | 2 +- test/Flurl.Test/Http/HeadersTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Flurl.Http/Flurl.Http.csproj b/src/Flurl.Http/Flurl.Http.csproj index ee479a80..bf383df2 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-pre6 + 4.0.0-pre7 Todd Menier A fluent, portable, testable HTTP client library. https://flurl.dev diff --git a/test/Flurl.Test/Http/HeadersTests.cs b/test/Flurl.Test/Http/HeadersTests.cs index 6795e174..07dcf98b 100644 --- a/test/Flurl.Test/Http/HeadersTests.cs +++ b/test/Flurl.Test/Http/HeadersTests.cs @@ -149,7 +149,7 @@ public class ClientHeadersTests : HeadersTestsBase protected override IFlurlClient CreateContainer() => new FlurlClient(); protected override IFlurlRequest GetRequest(IFlurlClient cli) => cli.Request("http://api.com"); - [Test] + [Test] // #778 public void can_copy_multi_value_header_from_HttpClient() { var userAgent = "Mozilla/5.0 (X11; Linux i686; rv:109.0) Gecko/20100101 Firefox/120.0"; var httpClient = new HttpClient();