Skip to content

Commit

Permalink
Add API allowing to disable retries for a given list of HTTP methods (#…
Browse files Browse the repository at this point in the history
…5634)

* Fixes #5248

Adds APIs allowing to disable automatic retries for a given list of HTTP methods

* Fixes #5248

Adds a check ensuring options.ShouldHandle is not null
  • Loading branch information
iliar-turdushev authored Nov 25, 2024
1 parent c08e5ac commit cfed375
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
using Polly;

namespace Microsoft.Extensions.Http.Resilience;

/// <summary>
/// Extensions for <see cref="HttpRetryStrategyOptions"/>.
/// </summary>
[Experimental(diagnosticId: DiagnosticIds.Experiments.Resilience, UrlFormat = DiagnosticIds.UrlFormat)]
public static class HttpRetryStrategyOptionsExtensions
{
#if !NET8_0_OR_GREATER
private static readonly HttpMethod _connect = new("CONNECT");
private static readonly HttpMethod _patch = new("PATCH");
#endif

/// <summary>
/// Disables retry attempts for POST, PATCH, PUT, DELETE, and CONNECT HTTP methods.
/// </summary>
/// <param name="options">The retry strategy options.</param>
public static void DisableForUnsafeHttpMethods(this HttpRetryStrategyOptions options)
{
options.DisableFor(
HttpMethod.Delete, HttpMethod.Post, HttpMethod.Put,
#if !NET8_0_OR_GREATER
_connect, _patch);
#else
HttpMethod.Connect, HttpMethod.Patch);
#endif
}

/// <summary>
/// Disables retry attempts for the given list of HTTP methods.
/// </summary>
/// <param name="options">The retry strategy options.</param>
/// <param name="methods">The list of HTTP methods.</param>
public static void DisableFor(this HttpRetryStrategyOptions options, params HttpMethod[] methods)
{
_ = Throw.IfNullOrEmpty(methods);

var shouldHandle = Throw.IfNullOrMemberNull(options, options?.ShouldHandle);

options.ShouldHandle = async args =>
{
var result = await shouldHandle(args).ConfigureAwait(args.Context.ContinueOnCapturedContext);
if (result &&
args.Outcome.Result is HttpResponseMessage response &&
response.RequestMessage is HttpRequestMessage request)
{
return !methods.Contains(request.Method);
}
return result;
};
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Net.Http;
using System.Threading.Tasks;
using Polly;
using Polly.Retry;
using Xunit;

namespace Microsoft.Extensions.Http.Resilience.Test.Polly;

public class HttpRetryStrategyOptionsExtensionsTests
{
[Fact]
public void DisableFor_RetryOptionsIsNull_Throws()
{
Assert.Throws<ArgumentNullException>(() => ((HttpRetryStrategyOptions)null!).DisableFor(HttpMethod.Get));
}

[Fact]
public void DisableFor_HttpMethodsIsNull_Throws()
{
Assert.Throws<ArgumentNullException>(() => new HttpRetryStrategyOptions().DisableFor(null!));
}

[Fact]
public void DisableFor_HttpMethodsIsEmptry_Throws()
{
Assert.Throws<ArgumentException>(() => new HttpRetryStrategyOptions().DisableFor([]));
}

[Fact]
public void DisableFor_ShouldHandleIsNull_Throws()
{
var options = new HttpRetryStrategyOptions { ShouldHandle = null! };
Assert.Throws<ArgumentException>(() => options.DisableFor(HttpMethod.Get));
}

[Theory]
[InlineData("POST", false)]
[InlineData("DELETE", false)]
[InlineData("GET", true)]
public async Task DisableFor_PositiveScenario(string httpMethod, bool shouldHandle)
{
var options = new HttpRetryStrategyOptions { ShouldHandle = _ => PredicateResult.True() };
options.DisableFor(HttpMethod.Post, HttpMethod.Delete);

using var request = new HttpRequestMessage { Method = new HttpMethod(httpMethod) };
using var response = new HttpResponseMessage { RequestMessage = request };

Assert.Equal(shouldHandle, await options.ShouldHandle(CreatePredicateArguments(response)));
}

[Fact]
public async Task DisableFor_RespectsOriginalShouldHandlePredicate()
{
var options = new HttpRetryStrategyOptions { ShouldHandle = _ => PredicateResult.False() };
options.DisableFor(HttpMethod.Post);

using var request = new HttpRequestMessage { Method = HttpMethod.Get };
using var response = new HttpResponseMessage { RequestMessage = request };

Assert.False(await options.ShouldHandle(CreatePredicateArguments(response)));
}

[Fact]
public async Task DisableFor_ResponseMessageIsNull_DoesNotDisableRetries()
{
var options = new HttpRetryStrategyOptions { ShouldHandle = _ => PredicateResult.True() };
options.DisableFor(HttpMethod.Post);

Assert.True(await options.ShouldHandle(CreatePredicateArguments(null)));
}

[Fact]
public async Task DisableFor_RequestMessageIsNull_DoesNotDisableRetries()
{
var options = new HttpRetryStrategyOptions { ShouldHandle = _ => PredicateResult.True() };
options.DisableFor(HttpMethod.Post);

using var response = new HttpResponseMessage { RequestMessage = null };

Assert.True(await options.ShouldHandle(CreatePredicateArguments(response)));
}

[Theory]
[InlineData("POST", false)]
[InlineData("DELETE", false)]
[InlineData("PUT", false)]
[InlineData("PATCH", false)]
[InlineData("CONNECT", false)]
[InlineData("GET", true)]
[InlineData("HEAD", true)]
[InlineData("TRACE", true)]
[InlineData("OPTIONS", true)]
public async Task DisableForUnsafeHttpMethods_PositiveScenario(string httpMethod, bool shouldHandle)
{
var options = new HttpRetryStrategyOptions { ShouldHandle = _ => PredicateResult.True() };
options.DisableForUnsafeHttpMethods();

using var request = new HttpRequestMessage { Method = new HttpMethod(httpMethod) };
using var response = new HttpResponseMessage { RequestMessage = request };

Assert.Equal(shouldHandle, await options.ShouldHandle(CreatePredicateArguments(response)));
}

private static RetryPredicateArguments<HttpResponseMessage> CreatePredicateArguments(HttpResponseMessage? response)
{
return new RetryPredicateArguments<HttpResponseMessage>(
ResilienceContextPool.Shared.Get(),
Outcome.FromResult(response),
attemptNumber: 1);
}
}

0 comments on commit cfed375

Please sign in to comment.