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/7] 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/7] 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/7] 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/7] 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/7] 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] From e11d8a76843cce25dc995053e0f8f031083770b9 Mon Sep 17 00:00:00 2001 From: Mateusz Noga-Wojtania Date: Wed, 19 Aug 2026 12:50:16 +0200 Subject: [PATCH 6/7] remove constructors and -1 status code usage --- .../Frends.HTTP.Request/Definitions/Result.cs | 26 ++++--------------- .../Helpers/ErrorHandler.cs | 16 +++++++----- .../Frends.HTTP.Request/Request.cs | 16 ++++++++++-- 3 files changed, 28 insertions(+), 30 deletions(-) diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Result.cs b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Result.cs index ce7d851..1dd648d 100644 --- a/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Result.cs +++ b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Result.cs @@ -11,45 +11,29 @@ public class Result /// Indicates whether the task completed successfully. /// /// true - public bool Success { get; private set; } + public bool Success { get; set; } /// /// Error details. Null when Success is true. /// /// null - public Error Error { get; private set; } + public Error Error { get; set; } /// /// Body of response /// /// {"id": "abcdefghijkl123456789", "success": true, "errors": []} - public dynamic Body { get; private set; } + public dynamic Body { get; set; } /// /// Headers of response /// /// {[ "content-type": "application/json", ... ]} - public Dictionary Headers { get; private set; } + public Dictionary Headers { get; set; } /// /// Statuscode of response /// /// 200 - public int StatusCode { get; private set; } - - internal Result(object body, Dictionary headers, int statusCode) - { - Success = true; - Error = null; - Body = body; - Headers = headers; - StatusCode = statusCode; - } - - internal Result(bool success, Error error) - { - Success = success; - Error = error; - StatusCode = -1; - } + public int StatusCode { get; set; } } diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Helpers/ErrorHandler.cs b/Frends.HTTP.Request/Frends.HTTP.Request/Helpers/ErrorHandler.cs index 35c2c2c..d53aa9a 100644 --- a/Frends.HTTP.Request/Frends.HTTP.Request/Helpers/ErrorHandler.cs +++ b/Frends.HTTP.Request/Frends.HTTP.Request/Helpers/ErrorHandler.cs @@ -42,12 +42,14 @@ private static Result ReturnResult(Exception exception, string customMessage = n ? exception.Message : $"{customMessage}: {exception.Message}"; - return new Result( - false, - new Error - { - Message = errorMessage, - AdditionalInfo = exception, - }); + return new Result + { + Success = false, + Error = 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 5606f36..e66c6ad 100644 --- a/Frends.HTTP.Request/Frends.HTTP.Request/Request.cs +++ b/Frends.HTTP.Request/Frends.HTTP.Request/Request.cs @@ -90,7 +90,13 @@ CancellationToken cancellationToken var hstatusCode = (int)responseMessage.StatusCode; var hheaders = GetResponseHeaderDictionary(responseMessage.Headers, responseMessage.Content.Headers); - response = new Result(hbody, hheaders, hstatusCode); + response = new Result + { + Success = true, + Body = hbody, + Headers = hheaders, + StatusCode = hstatusCode + }; break; case ReturnFormat.JToken: @@ -102,7 +108,13 @@ CancellationToken cancellationToken var rstatusCode = (int)responseMessage.StatusCode; var rheaders = GetResponseHeaderDictionary(responseMessage.Headers, responseMessage.Content.Headers); - response = new Result(rbody, rheaders, rstatusCode); + response = new Result + { + Success = true, + Body = rbody, + Headers = rheaders, + StatusCode = rstatusCode, + }; break; default: throw new InvalidOperationException(); From 8de605fd27e517a2c5f7d7838c1234762da1cd1d Mon Sep 17 00:00:00 2001 From: Mateusz Noga-Wojtania Date: Wed, 19 Aug 2026 13:00:20 +0200 Subject: [PATCH 7/7] linter fix --- .../Frends.HTTP.Request/Helpers/ErrorHandler.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Helpers/ErrorHandler.cs b/Frends.HTTP.Request/Frends.HTTP.Request/Helpers/ErrorHandler.cs index d53aa9a..8dfd234 100644 --- a/Frends.HTTP.Request/Frends.HTTP.Request/Helpers/ErrorHandler.cs +++ b/Frends.HTTP.Request/Frends.HTTP.Request/Helpers/ErrorHandler.cs @@ -46,10 +46,10 @@ private static Result ReturnResult(Exception exception, string customMessage = n { Success = false, Error = new Error - { - Message = errorMessage, - AdditionalInfo = exception, - }, + { + Message = errorMessage, + AdditionalInfo = exception, + }, }; } }