There is no reason to limit users by only supporting a subset of HttpContent types. Further, support can be added for HttpRequestMessage objects whose content is of type StreamContent while still defaulting to the existing shallow copy logic for HttpRequestMessage objects whose content is of any other HttpContent type. In this way, the change can be made fully backwards compatible with existing APIs and with no side effects for existing users of the AddStandardHedgingHandler() API.
public static IStandardHedgingHandlerBuilder AddStandardHedgingHandler(this IHttpClientBuilder builder)
{
_ = Throw.IfNull(builder);
var optionsName = builder.Name;
var routingBuilder = new RoutingStrategyBuilder(builder.Name, builder.Services);
builder.Services.TryAddSingleton<Randomizer>();
_ = builder.Services.AddOptionsWithValidateOnStart<HttpStandardHedgingResilienceOptions, HttpStandardHedgingResilienceOptionsValidator>(optionsName);
_ = builder.Services.AddOptionsWithValidateOnStart<HttpStandardHedgingResilienceOptions, HttpStandardHedgingResilienceOptionsCustomValidator>(optionsName);
_ = builder.Services.PostConfigure<HttpStandardHedgingResilienceOptions>(optionsName, options =>
{
options.Hedging.ActionGenerator = args =>
{
if (!args.PrimaryContext.Properties.TryGetValue(ResilienceKeys.RequestSnapshot, out var snapshot))
{
Throw.InvalidOperationException("Request message snapshot is not attached to the resilience context.");
}
// if a routing strategy has been configured but it does not return the next route, then no more routes
// are availabe, stop hedging
Uri? route;
if (args.PrimaryContext.Properties.TryGetValue(ResilienceKeys.RoutingStrategy, out var routingPipeline))
{
if (!routingPipeline.TryGetNextRoute(out route))
{
return null;
}
}
else
{
route = null;
}
return async () =>
{
Outcome<HttpResponseMessage>? actionResult = null;
try
{
var requestMessage = await snapshot.CreateRequestMessageAsync().ConfigureAwait(false);
// The secondary request message should use the action resilience context
requestMessage.SetResilienceContext(args.ActionContext);
// replace the request message
args.ActionContext.Properties.Set(ResilienceKeys.RequestMessage, requestMessage);
if (route != null)
{
// replace the RequestUri of the request per the routing strategy
requestMessage.RequestUri = requestMessage.RequestUri!.ReplaceHost(route);
}
}
catch (IOException e)
{
actionResult = Outcome.FromException<HttpResponseMessage>(e);
}
return actionResult ?? await args.Callback(args.ActionContext).ConfigureAwait(args.ActionContext.ContinueOnCapturedContext);
};
};
});
// configure outer handler
var outerHandler = builder.AddResilienceHandler(HedgingConstants.HandlerPostfix, (builder, context) =>
{
var options = context.GetOptions<HttpStandardHedgingResilienceOptions>(optionsName);
context.EnableReloads<HttpStandardHedgingResilienceOptions>(optionsName);
var routingOptions = context.GetOptions<RequestRoutingOptions>(routingBuilder.Name);
_ = builder
.AddStrategy(_ => new RoutingResilienceStrategy(routingOptions.RoutingStrategyProvider))
.AddStrategy(_ => new RequestMessageSnapshotStrategy())
.AddTimeout(options.TotalRequestTimeout)
.AddHedging(options.Hedging);
});
// configure inner handler
var innerBuilder = builder.AddResilienceHandler(
HedgingConstants.InnerHandlerPostfix,
(builder, context) =>
{
var options = context.GetOptions<HttpStandardHedgingResilienceOptions>(optionsName);
context.EnableReloads<HttpStandardHedgingResilienceOptions>(optionsName);
_ = builder
.AddRateLimiter(options.Endpoint.RateLimiter)
.AddCircuitBreaker(options.Endpoint.CircuitBreaker)
.AddTimeout(options.Endpoint.Timeout);
})
.SelectPipelineByAuthority();
return new StandardHedgingHandlerBuilder(builder.Name, builder.Services, routingBuilder);
}
...
Background and motivation
Many clients trying to use the
AddStandardHedgingHandler()
resilience API based on top of Polly v8 have requirements that force them to sendHttpRequestMessage
objects that containStreamContent
. Today, if a client built with anIHttpClientBuilder
that was configured with a resilience handler via theAddStandardHedgingHandler()
API attempts to send anHttpRequestMessage
object that containsStreamContent
to a downstream service, then anInvalidOperationException
will be thrown. This exception is thrown by theInitialize()
method in RequestMessageSnapshot.cs: https://github.com/dotnet/extensions/blob/10681a1cdb1e044b05341150203b94d5eec41557/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/RequestMessageSnapshot.cs#L73-L80There is no reason to limit users by only supporting a subset of
HttpContent
types. Further, support can be added forHttpRequestMessage
objects whose content is of typeStreamContent
while still defaulting to the existing shallow copy logic forHttpRequestMessage
objects whose content is of any otherHttpContent
type. In this way, the change can be made fully backwards compatible with existing APIs and with no side effects for existing users of theAddStandardHedgingHandler()
API.Feature Proposal
Proposed Changes to RequestMessageSnapshot.cs:
Proposed Changes to ResilienceHttpClientBuilderExtensions.Hedging.cs:
Proposed Changes to RequestMessageSnapshotStrategy.cs: