Skip to content
Open
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
7 changes: 7 additions & 0 deletions Frends.HTTP.Request/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
59 changes: 59 additions & 0 deletions Frends.HTTP.Request/Frends.HTTP.Request.Tests/ErrorHandlerTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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<Header>(),
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<Exception>(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);
Assert.That(result.Error.Message, Is.Not.Null.And.Not.Empty);
Assert.That(result.Error.AdditionalInfo, Is.Not.Null);
}

[Test]
public void Should_Use_Custom_ErrorMessageOnFailure()
{
var options = DefaultOptions();
options.ErrorMessageOnFailure = CustomErrorMessage;
var ex = Assert.ThrowsAsync<Exception>(async () =>
await HTTP.Request(InvalidInput(), options, CancellationToken.None));
Assert.That(ex, Is.Not.Null);
Assert.That(ex.Message, Does.Contain(CustomErrorMessage));
}
}
9 changes: 6 additions & 3 deletions Frends.HTTP.Request/Frends.HTTP.Request.Tests/UnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,11 @@ public void RequestShouldThrowExceptionIfUrlEmpty()
ThrowExceptionOnErrorResponse = true
};

var ex = Assert.ThrowsAsync<ArgumentNullException>(async () =>
var ex = Assert.ThrowsAsync<Exception>(async () =>
await HTTP.Request(input, options, CancellationToken.None));

ClassicAssert.IsTrue(ex.Message.Contains("Url can not be empty."));
Assert.That(ex.InnerException, Is.TypeOf<ArgumentNullException>());
}

[TestMethod]
Expand All @@ -108,11 +109,12 @@ public void RequestShouldThrowExceptionIfOptionIsSet()
ThrowExceptionOnErrorResponse = true
};

var ex = Assert.ThrowsAsync<WebException>(async () =>
var ex = Assert.ThrowsAsync<Exception>(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<WebException>());
}

[TestMethod]
Expand Down Expand Up @@ -230,10 +232,11 @@ public void RequestShouldAddClientCertificate()
CertificateThumbprint = thumbprint
};

var ex = Assert.ThrowsAsync<FileNotFoundException>(async () =>
var ex = Assert.ThrowsAsync<Exception>(async () =>
await HTTP.Request(input, options, CancellationToken.None));

ClassicAssert.IsTrue(ex.Message.Contains($"Certificate with thumbprint: '{thumbprint}' not"));
Assert.That(ex.InnerException, Is.TypeOf<FileNotFoundException>());
}

[TestMethod]
Expand Down
21 changes: 21 additions & 0 deletions Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Error.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;

Check warning on line 1 in Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Error.cs

View workflow job for this annotation

GitHub Actions / build / Build on ubuntu-22.04

Using directive should appear within a namespace declaration (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md)

Check warning on line 1 in Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Error.cs

View workflow job for this annotation

GitHub Actions / build / Build on ubuntu-22.04

The file header is missing or not located at the top of the file. (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1633.md)

Check warning on line 1 in Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Error.cs

View workflow job for this annotation

GitHub Actions / build / Build on ubuntu-22.04

The file header is missing or not located at the top of the file.

Check warning on line 1 in Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Error.cs

View workflow job for this annotation

GitHub Actions / build / Build on ubuntu-22.04

Using directive should appear within a namespace declaration (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md)

namespace Frends.HTTP.Request.Definitions;

/// <summary>
/// Error details returned when the task fails and ThrowErrorOnFailure is false.
/// </summary>
public class Error
{
/// <summary>
/// Error message.
/// </summary>
/// <example>An error occurred while processing the request.</example>
public string Message { get; internal set; }

/// <summary>
/// Additional error information, such as the original exception.
/// </summary>
/// <example>null</example>
public Exception AdditionalInfo { get; internal set; }
}
2 changes: 2 additions & 0 deletions Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Header.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Frends.HTTP.Request.Definitions;

Check warning on line 1 in Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Header.cs

View workflow job for this annotation

GitHub Actions / build / Build on ubuntu-22.04

The file header is missing or not located at the top of the file. (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1633.md)

Check warning on line 1 in Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Header.cs

View workflow job for this annotation

GitHub Actions / build / Build on ubuntu-22.04

The file header is missing or not located at the top of the file.

/// <summary>
/// Request header.
Expand All @@ -8,10 +8,12 @@
/// <summary>
/// Name of header.
/// </summary>
/// <example>Content-Type</example>
public string Name { get; set; }

/// <summary>
/// Value of header.
/// </summary>
/// <example>application/json</example>
public string Value { get; set; }
}
15 changes: 15 additions & 0 deletions Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Options.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.ComponentModel;

Check warning on line 1 in Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Options.cs

View workflow job for this annotation

GitHub Actions / build / Build on ubuntu-22.04

The file header is missing or not located at the top of the file.

Check warning on line 1 in Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Options.cs

View workflow job for this annotation

GitHub Actions / build / Build on ubuntu-22.04

Using directive should appear within a namespace declaration (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md)
using System.ComponentModel.DataAnnotations;

Check warning on line 2 in Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Options.cs

View workflow job for this annotation

GitHub Actions / build / Build on ubuntu-22.04

Using directive should appear within a namespace declaration (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md)

namespace Frends.HTTP.Request.Definitions;
Expand Down Expand Up @@ -188,4 +188,19 @@
/// <example>Default</example>
[DefaultValue(SslVersion.Default)]
public SslVersion SslProtocolVersion { get; set; } = SslVersion.Default;

/// <summary>
/// Whether to throw an error on failure.
/// </summary>
/// <example>true</example>
[DefaultValue(true)]
public bool ThrowErrorOnFailure { get; set; } = true;

/// <summary>
/// Overrides the error message on failure.
/// </summary>
/// <example>HTTP request failed: connection refused</example>
[DisplayFormat(DataFormatString = "Text")]
[DefaultValue("")]
public string ErrorMessageOnFailure { get; set; } = string.Empty;
}
24 changes: 19 additions & 5 deletions Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Result.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Collections.Generic;

Check warning on line 1 in Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Result.cs

View workflow job for this annotation

GitHub Actions / build / Build on ubuntu-22.04

Using directive should appear within a namespace declaration (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md)

namespace Frends.HTTP.Request.Definitions;

Expand All @@ -7,6 +7,18 @@
/// </summary>
public class Result
{
/// <summary>
/// Indicates whether the task completed successfully.
/// </summary>
/// <example>true</example>
public bool Success { get; private set; }

/// <summary>
/// Error details. Null when Success is true.
/// </summary>
/// <example>null</example>
public Error Error { get; private set; }

/// <summary>
/// Body of response
/// </summary>
Expand All @@ -25,17 +37,19 @@
/// <example>200</example>
public int StatusCode { get; private set; }

internal Result(string body, Dictionary<string, string> headers, int statusCode)
internal Result(object body, Dictionary<string, string> headers, int statusCode)
{
Success = true;
Error = null;
Body = body;
Headers = headers;
StatusCode = statusCode;
}

internal Result(object body, Dictionary<string, string> headers, int statusCode)
internal Result(bool success, Error error)
{
Body = body;
Headers = headers;
StatusCode = statusCode;
Success = success;
Error = error;
StatusCode = -1;
}
}
14 changes: 11 additions & 3 deletions Frends.HTTP.Request/Frends.HTTP.Request/Frends.HTTP.Request.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>1.12.0</Version>
<Version>1.13.0</Version>
<Authors>Frends</Authors>
<Copyright>Frends</Copyright>
<Company>Frends</Company>
Expand All @@ -16,16 +16,24 @@
</PropertyGroup>

<ItemGroup>
<None Include="FrendsTaskMetadata.json" Pack="true" PackagePath="/">
<AdditionalFiles Include="FrendsTaskMetadata.json" Pack="true" PackagePath="/">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</AdditionalFiles>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="System.Runtime.Caching" Version="9.0.3" />
<PackageReference Include="System.DirectoryServices" Version="9.0.3" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FrendsTaskAnalyzers" Version="1.*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
53 changes: 53 additions & 0 deletions Frends.HTTP.Request/Frends.HTTP.Request/Helpers/ErrorHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using Frends.HTTP.Request.Definitions;

namespace Frends.HTTP.Request.Helpers;

/// <summary>
/// Converts an exception into a failed Result object or rethrows based on task options.
/// </summary>
internal static class ErrorHandler
{
/// <param name="exception">The exception to handle.</param>
/// <param name="options">Task options that control whether failures are returned as a Result object or thrown.</param>
/// <param name="throwCanceled">
/// When true, an OperationCanceledException is rethrown immediately.
/// When false, cancellation is handled like any other failure.
/// </param>
/// <returns>A failed Result object when the exception is handled instead of rethrown.</returns>
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,
});
}
}
5 changes: 5 additions & 0 deletions Frends.HTTP.Request/Frends.HTTP.Request/Request.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]

Expand Down Expand Up @@ -115,6 +116,10 @@ CancellationToken cancellationToken

return response;
}
catch (Exception ex)
{
return ex.Handle(options);
}
finally
{
httpContent?.Dispose();
Expand Down
Loading