-
Notifications
You must be signed in to change notification settings - Fork 759
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add API allowing to disable retries for a given list of HTTP methods (#…
…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
1 parent
c08e5ac
commit cfed375
Showing
2 changed files
with
181 additions
and
0 deletions.
There are no files selected for viewing
66 changes: 66 additions & 0 deletions
66
...ibraries/Microsoft.Extensions.Http.Resilience/Polly/HttpRetryStrategyOptionsExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; | ||
} | ||
} | ||
|
115 changes: 115 additions & 0 deletions
115
...crosoft.Extensions.Http.Resilience.Tests/Polly/HttpRetryStrategyOptionsExtensionsTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |