From deaf1cfbe588eb9b3285460a18feaf84461a32ee Mon Sep 17 00:00:00 2001 From: Adam Hammond Date: Mon, 22 Apr 2024 15:31:48 -0700 Subject: [PATCH] Fixes #5105 Add support for HttpRequestMessage objects containing StreamContent to the AddStandardHedgingHandler() resilience API. This change does not update any public API contracts. It updates internal and private API contracts only. This is a small commit to resolve comments made on the PR. --- .../Internal/RequestMessageSnapshot.cs | 6 ++-- .../RequestMessageSnapshotStrategyTests.cs | 36 +++++++++++++++++++ .../Resilience/RequestMessageSnapshotTests.cs | 2 +- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/RequestMessageSnapshot.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/RequestMessageSnapshot.cs index b44b65b05b3..43f4d412ed5 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/RequestMessageSnapshot.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/RequestMessageSnapshot.cs @@ -25,7 +25,7 @@ internal sealed class RequestMessageSnapshot : IResettable, IDisposable private HttpContent? _content; [System.Diagnostics.CodeAnalysis.SuppressMessage("Resilience", "EA0014:The async method doesn't support cancellation", Justification = "Past the point of no cancellation.")] - public static async Task CreateAsync(HttpRequestMessage request) + public static async ValueTask CreateAsync(HttpRequestMessage request) { _ = Throw.IfNull(request); @@ -35,7 +35,7 @@ public static async Task CreateAsync(HttpRequestMessage } [System.Diagnostics.CodeAnalysis.SuppressMessage("Resilience", "EA0014:The async method doesn't support cancellation", Justification = "Past the point of no cancellation.")] - public async Task CreateRequestMessageAsync() + public async ValueTask CreateRequestMessageAsync() { if (!IsInitialized()) { @@ -101,7 +101,7 @@ bool IResettable.TryReset() void IDisposable.Dispose() => _snapshots.Return(this); [System.Diagnostics.CodeAnalysis.SuppressMessage("Resilience", "EA0014:The async method doesn't support cancellation", Justification = "Past the point of no cancellation.")] - private static async Task<(HttpContent? content, HttpContent? clonedContent)> CloneContentAsync(HttpContent? content) + private static async ValueTask<(HttpContent? content, HttpContent? clonedContent)> CloneContentAsync(HttpContent? content) { HttpContent? clonedContent = null; if (content is not null) diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Internal/RequestMessageSnapshotStrategyTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Internal/RequestMessageSnapshotStrategyTests.cs index cafa30e43a7..ff66487a4bc 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Internal/RequestMessageSnapshotStrategyTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Internal/RequestMessageSnapshotStrategyTests.cs @@ -2,7 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.IO; using System.Net.Http; +using System.Text; +using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Microsoft.Extensions.Http.Resilience.Internal; @@ -39,5 +42,38 @@ public void ExecuteAsync_RequestMessageNotFound_Throws() strategy.Invoking(s => s.Execute(() => { })).Should().Throw(); } + [Fact] + public async Task ExecuteCoreAsync_IOExceptionThrownWhenCreatingSnapshot_ReturnsExceptionOutcome() + { + var strategy = Create(); + var context = ResilienceContextPool.Shared.Get(); + using var request = new HttpRequestMessage(HttpMethod.Post, new Uri("https://www.example.com/some-resource")); + using var stream = new StreamTestHelper("some stream content"); + request.Content = new StreamContent(stream); + context.Properties.Set(ResilienceKeys.RequestMessage, request); + + _ = await Assert.ThrowsAsync(async () => await strategy.ExecuteAsync(context => default, context)); + } + private static ResiliencePipeline Create() => new ResiliencePipelineBuilder().AddStrategy(_ => new RequestMessageSnapshotStrategy(), Mock.Of()).Build(); + + private class StreamTestHelper : MemoryStream + { + public StreamTestHelper(string str) + : base(Encoding.UTF8.GetBytes(str)) + { + } + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => throw new IOException(); + +#if NET5_0_OR_GREATER + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) => throw new IOException(); + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => throw new IOException(); +#else + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw new IOException(); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw new IOException(); +#endif + } } diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/RequestMessageSnapshotTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/RequestMessageSnapshotTests.cs index 3e57ad1df5b..b9d7844fa0a 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/RequestMessageSnapshotTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/RequestMessageSnapshotTests.cs @@ -123,7 +123,7 @@ public async Task CreateRequestMessageAsync_SnapshotIsReset_ThrowsException() AddContentHeaders(_requestMessage!.Content); using RequestMessageSnapshot snapshot = await RequestMessageSnapshot.CreateAsync(_requestMessage).ConfigureAwait(false); ((IResettable)snapshot).TryReset(); - _ = await Assert.ThrowsAsync(snapshot.CreateRequestMessageAsync); + _ = await Assert.ThrowsAsync(async () => await snapshot.CreateRequestMessageAsync().ConfigureAwait(false)); } public void Dispose()