From 94bc5d66813a0172d15405eca2a46e40981aad43 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:03:45 +0000 Subject: [PATCH 1/5] Initial plan From 721784e36d265430c853b68c5afee3434b8f8988 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:09:57 +0000 Subject: [PATCH 2/5] fix: resolve FT0011, FT0014, FT0015, FT0017 compliance findings in Frends.HTTP.Request Co-authored-by: MatteoDelOmbra <44415151+MatteoDelOmbra@users.noreply.github.com> --- Frends.HTTP.Request/CHANGELOG.md | 7 +++ .../ErrorHandlerTest.cs | 57 +++++++++++++++++++ .../Frends.HTTP.Request/Definitions/Error.cs | 19 +++++++ .../Frends.HTTP.Request/Definitions/Header.cs | 2 + .../Definitions/Options.cs | 15 +++++ .../Frends.HTTP.Request/Definitions/Result.cs | 23 ++++++-- .../Frends.HTTP.Request.csproj | 14 ++++- .../Helpers/ErrorHandler.cs | 53 +++++++++++++++++ .../Frends.HTTP.Request/Request.cs | 5 ++ 9 files changed, 187 insertions(+), 8 deletions(-) create mode 100644 Frends.HTTP.Request/Frends.HTTP.Request.Tests/ErrorHandlerTest.cs create mode 100644 Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Error.cs create mode 100644 Frends.HTTP.Request/Frends.HTTP.Request/Helpers/ErrorHandler.cs diff --git a/Frends.HTTP.Request/CHANGELOG.md b/Frends.HTTP.Request/CHANGELOG.md index d744f1d..38aa80a 100644 --- a/Frends.HTTP.Request/CHANGELOG.md +++ b/Frends.HTTP.Request/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.13.0] - 2026-08-17 + +### Added + +- Added `ThrowErrorOnFailure` and `ErrorMessageOnFailure` options to control error handling: when `ThrowErrorOnFailure` is set to false, the task returns a `Result` with `Success = false` and an `Error` object instead of throwing an exception. +- The `Result` type now includes `Success` and `Error` properties to indicate task outcome. + ## [1.12.0] - 2026-06-12 ### Fixed diff --git a/Frends.HTTP.Request/Frends.HTTP.Request.Tests/ErrorHandlerTest.cs b/Frends.HTTP.Request/Frends.HTTP.Request.Tests/ErrorHandlerTest.cs new file mode 100644 index 0000000..6446acf --- /dev/null +++ b/Frends.HTTP.Request/Frends.HTTP.Request.Tests/ErrorHandlerTest.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading; +using Frends.HTTP.Request.Definitions; +using NUnit.Framework; + +namespace Frends.HTTP.Request.Tests; + +[TestFixture] +internal class ErrorHandlerTest +{ + private const string InvalidUrl = "http://thisdomaindoesnotexist.invalid/"; + private const string CustomErrorMessage = "CustomErrorMessage"; + + private static Input InvalidInput() => new Input + { + Method = Method.GET, + Url = InvalidUrl, + Headers = Array.Empty
(), + Message = string.Empty, + }; + + private static Options DefaultOptions() => new Options + { + ConnectionTimeoutSeconds = 5, + ThrowErrorOnFailure = true, + ErrorMessageOnFailure = string.Empty, + }; + + [Test] + public void Should_Throw_Error_When_ThrowErrorOnFailure_Is_True() + { + var ex = Assert.ThrowsAsync(async () => + await HTTP.Request(InvalidInput(), DefaultOptions(), CancellationToken.None)); + Assert.That(ex, Is.Not.Null); + } + + [Test] + public async System.Threading.Tasks.Task Should_Return_Failed_Result_When_ThrowErrorOnFailure_Is_False() + { + var options = DefaultOptions(); + options.ThrowErrorOnFailure = false; + var result = await HTTP.Request(InvalidInput(), options, CancellationToken.None); + Assert.That(result.Success, Is.False); + Assert.That(result.Error, Is.Not.Null); + } + + [Test] + public void Should_Use_Custom_ErrorMessageOnFailure() + { + var options = DefaultOptions(); + options.ErrorMessageOnFailure = CustomErrorMessage; + var ex = Assert.ThrowsAsync(async () => + await HTTP.Request(InvalidInput(), options, CancellationToken.None)); + Assert.That(ex, Is.Not.Null); + Assert.That(ex.Message, Does.Contain(CustomErrorMessage)); + } +} diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Error.cs b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Error.cs new file mode 100644 index 0000000..62a2bad --- /dev/null +++ b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Error.cs @@ -0,0 +1,19 @@ +namespace Frends.HTTP.Request.Definitions; + +/// +/// Error details returned when the task fails and ThrowErrorOnFailure is false. +/// +public class Error +{ + /// + /// Error message. + /// + /// An error occurred while processing the request. + public string Message { get; internal set; } + + /// + /// Additional error information, such as the original exception. + /// + /// null + public object AdditionalInfo { get; internal set; } +} diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Header.cs b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Header.cs index fac879e..ee626ef 100644 --- a/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Header.cs +++ b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Header.cs @@ -8,10 +8,12 @@ public class Header /// /// Name of header. /// + /// Content-Type public string Name { get; set; } /// /// Value of header. /// + /// application/json public string Value { get; set; } } diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Options.cs b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Options.cs index 96f564b..476c390 100644 --- a/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Options.cs +++ b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Options.cs @@ -188,4 +188,19 @@ public class Options /// Default [DefaultValue(SslVersion.Default)] public SslVersion SslProtocolVersion { get; set; } = SslVersion.Default; + + /// + /// Whether to throw an error on failure. + /// + /// true + [DefaultValue(true)] + public bool ThrowErrorOnFailure { get; set; } = true; + + /// + /// Overrides the error message on failure. + /// + /// HTTP request failed: connection refused + [DisplayFormat(DataFormatString = "Text")] + [DefaultValue("")] + public string ErrorMessageOnFailure { get; set; } = string.Empty; } diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Result.cs b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Result.cs index 6d7814c..fe5fe30 100644 --- a/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Result.cs +++ b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Result.cs @@ -7,6 +7,18 @@ namespace Frends.HTTP.Request.Definitions; /// public class Result { + /// + /// Indicates whether the task completed successfully. + /// + /// true + public bool Success { get; private set; } + + /// + /// Error details. Null when Success is true. + /// + /// null + public Error Error { get; private set; } + /// /// Body of response /// @@ -25,17 +37,18 @@ public class Result /// 200 public int StatusCode { get; private set; } - internal Result(string body, Dictionary headers, int statusCode) + internal Result(object body, Dictionary headers, int statusCode) { + Success = true; + Error = null; Body = body; Headers = headers; StatusCode = statusCode; } - internal Result(object body, Dictionary headers, int statusCode) + internal Result(bool success, Error error) { - Body = body; - Headers = headers; - StatusCode = statusCode; + Success = success; + Error = error; } } diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Frends.HTTP.Request.csproj b/Frends.HTTP.Request/Frends.HTTP.Request/Frends.HTTP.Request.csproj index 99453c3..f19245c 100644 --- a/Frends.HTTP.Request/Frends.HTTP.Request/Frends.HTTP.Request.csproj +++ b/Frends.HTTP.Request/Frends.HTTP.Request/Frends.HTTP.Request.csproj @@ -2,7 +2,7 @@ net8.0 - 1.12.0 + 1.13.0 Frends Frends Frends @@ -16,9 +16,9 @@ - + PreserveNewest - + @@ -26,6 +26,14 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Helpers/ErrorHandler.cs b/Frends.HTTP.Request/Frends.HTTP.Request/Helpers/ErrorHandler.cs new file mode 100644 index 0000000..35c2c2c --- /dev/null +++ b/Frends.HTTP.Request/Frends.HTTP.Request/Helpers/ErrorHandler.cs @@ -0,0 +1,53 @@ +using System; +using Frends.HTTP.Request.Definitions; + +namespace Frends.HTTP.Request.Helpers; + +/// +/// Converts an exception into a failed Result object or rethrows based on task options. +/// +internal static class ErrorHandler +{ + /// The exception to handle. + /// Task options that control whether failures are returned as a Result object or thrown. + /// + /// When true, an OperationCanceledException is rethrown immediately. + /// When false, cancellation is handled like any other failure. + /// + /// A failed Result object when the exception is handled instead of rethrown. + internal static Result Handle(this Exception exception, Options options, bool throwCanceled = true) + { + ThrowIfCanceled(exception, throwCanceled); + if (options.ThrowErrorOnFailure) ThrowBaseException(exception, options.ErrorMessageOnFailure); + + return ReturnResult(exception, options.ErrorMessageOnFailure); + } + + private static void ThrowIfCanceled(Exception exception, bool throwCanceled = true) + { + if (throwCanceled && exception is OperationCanceledException) throw exception; + } + + private static void ThrowBaseException(Exception exception, string customMessage = null) + { + if (string.IsNullOrEmpty(customMessage)) + throw new Exception(exception.Message, exception); + + throw new Exception(customMessage, exception); + } + + private static Result ReturnResult(Exception exception, string customMessage = null) + { + var errorMessage = string.IsNullOrEmpty(customMessage) + ? exception.Message + : $"{customMessage}: {exception.Message}"; + + return new Result( + false, + new Error + { + Message = errorMessage, + AdditionalInfo = exception, + }); + } +} diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Request.cs b/Frends.HTTP.Request/Frends.HTTP.Request/Request.cs index 9719dba..5606f36 100644 --- a/Frends.HTTP.Request/Frends.HTTP.Request/Request.cs +++ b/Frends.HTTP.Request/Frends.HTTP.Request/Request.cs @@ -16,6 +16,7 @@ using System.Diagnostics.CodeAnalysis; using System.Security.Cryptography.X509Certificates; using Frends.HTTP.Request.Definitions; +using Frends.HTTP.Request.Helpers; [assembly: InternalsVisibleTo("Frends.HTTP.Request.Tests")] @@ -115,6 +116,10 @@ CancellationToken cancellationToken return response; } + catch (Exception ex) + { + return ex.Handle(options); + } finally { httpContent?.Dispose(); From c9e891948825ada5b73346d083e47f3884e7f8e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:10:58 +0000 Subject: [PATCH 3/5] fix: improve ErrorHandlerTest assertions and Result failure sentinel value Co-authored-by: MatteoDelOmbra <44415151+MatteoDelOmbra@users.noreply.github.com> --- .../Frends.HTTP.Request.Tests/ErrorHandlerTest.cs | 2 ++ Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Result.cs | 1 + 2 files changed, 3 insertions(+) diff --git a/Frends.HTTP.Request/Frends.HTTP.Request.Tests/ErrorHandlerTest.cs b/Frends.HTTP.Request/Frends.HTTP.Request.Tests/ErrorHandlerTest.cs index 6446acf..ff1b1f7 100644 --- a/Frends.HTTP.Request/Frends.HTTP.Request.Tests/ErrorHandlerTest.cs +++ b/Frends.HTTP.Request/Frends.HTTP.Request.Tests/ErrorHandlerTest.cs @@ -42,6 +42,8 @@ public async System.Threading.Tasks.Task Should_Return_Failed_Result_When_ThrowE var result = await HTTP.Request(InvalidInput(), options, CancellationToken.None); Assert.That(result.Success, Is.False); Assert.That(result.Error, Is.Not.Null); + Assert.That(result.Error.Message, Is.Not.Null.And.Not.Empty); + Assert.That(result.Error.AdditionalInfo, Is.Not.Null); } [Test] diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Result.cs b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Result.cs index fe5fe30..ce7d851 100644 --- a/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Result.cs +++ b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Result.cs @@ -50,5 +50,6 @@ internal Result(bool success, Error error) { Success = success; Error = error; + StatusCode = -1; } } From cd72b44466e930c359e0f361b86c8785d6a6842b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Aug 2026 12:02:11 +0000 Subject: [PATCH 4/5] Change Error.AdditionalInfo type from object to Exception Co-authored-by: MatteoDelOmbra <44415151+MatteoDelOmbra@users.noreply.github.com> --- Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Error.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Error.cs b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Error.cs index 62a2bad..c348cf5 100644 --- a/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Error.cs +++ b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Error.cs @@ -1,3 +1,5 @@ +using System; + namespace Frends.HTTP.Request.Definitions; /// @@ -15,5 +17,5 @@ public class Error /// Additional error information, such as the original exception. /// /// null - public object AdditionalInfo { get; internal set; } + public Exception AdditionalInfo { get; internal set; } } From 55449f1829ad2175f09ab4948e6218056b456afe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Aug 2026 12:04:59 +0000 Subject: [PATCH 5/5] test: align Request exception assertions with wrapped errors Co-authored-by: MatteoDelOmbra <44415151+MatteoDelOmbra@users.noreply.github.com> --- .../Frends.HTTP.Request.Tests/UnitTests.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Frends.HTTP.Request/Frends.HTTP.Request.Tests/UnitTests.cs b/Frends.HTTP.Request/Frends.HTTP.Request.Tests/UnitTests.cs index 5863469..b2f0204 100644 --- a/Frends.HTTP.Request/Frends.HTTP.Request.Tests/UnitTests.cs +++ b/Frends.HTTP.Request/Frends.HTTP.Request.Tests/UnitTests.cs @@ -86,10 +86,11 @@ public void RequestShouldThrowExceptionIfUrlEmpty() ThrowExceptionOnErrorResponse = true }; - var ex = Assert.ThrowsAsync(async () => + var ex = Assert.ThrowsAsync(async () => await HTTP.Request(input, options, CancellationToken.None)); ClassicAssert.IsTrue(ex.Message.Contains("Url can not be empty.")); + Assert.That(ex.InnerException, Is.TypeOf()); } [TestMethod] @@ -108,11 +109,12 @@ public void RequestShouldThrowExceptionIfOptionIsSet() ThrowExceptionOnErrorResponse = true }; - var ex = Assert.ThrowsAsync(async () => + var ex = Assert.ThrowsAsync(async () => await HTTP.Request(input, options, CancellationToken.None)); ClassicAssert.IsTrue( ex.Message.Contains($"Request to '{BasePath}/invalid' failed with status code 404")); + Assert.That(ex.InnerException, Is.TypeOf()); } [TestMethod] @@ -230,10 +232,11 @@ public void RequestShouldAddClientCertificate() CertificateThumbprint = thumbprint }; - var ex = Assert.ThrowsAsync(async () => + var ex = Assert.ThrowsAsync(async () => await HTTP.Request(input, options, CancellationToken.None)); ClassicAssert.IsTrue(ex.Message.Contains($"Certificate with thumbprint: '{thumbprint}' not")); + Assert.That(ex.InnerException, Is.TypeOf()); } [TestMethod]