diff --git a/Frends.HTTP.Request/Frends.HTTP.Request.Tests/UnitTests.cs b/Frends.HTTP.Request/Frends.HTTP.Request.Tests/UnitTests.cs index b2f0204..639db4f 100644 --- a/Frends.HTTP.Request/Frends.HTTP.Request.Tests/UnitTests.cs +++ b/Frends.HTTP.Request/Frends.HTTP.Request.Tests/UnitTests.cs @@ -93,6 +93,28 @@ public void RequestShouldThrowExceptionIfUrlEmpty() Assert.That(ex.InnerException, Is.TypeOf()); } + [TestMethod] + public async Task RequestShouldFollowRedirectForRequestWithBody() + { + var input = GetInputParams( + method: Method.Method.POST, + url: $"{BasePath}/redirect-to?url=/anything&status_code=307", + message: "{\"value\":\"redirected\"}", + new Header { Name = "Content-Type", Value = "application/json" }); + + var options = new Options + { + ConnectionTimeoutSeconds = 60, + FollowRedirects = true, + }; + + var result = await HTTP.Request(input, options, CancellationToken.None); + var body = JObject.Parse((string)result.Body); + + ClassicAssert.AreEqual(200, result.StatusCode); + ClassicAssert.AreEqual("redirected", body["json"]?["value"]?.Value()); + } + [TestMethod] public void RequestShouldThrowExceptionIfOptionIsSet() { diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Request.cs b/Frends.HTTP.Request/Frends.HTTP.Request/Request.cs index e66c6ad..8cb7fbc 100644 --- a/Frends.HTTP.Request/Frends.HTTP.Request/Request.cs +++ b/Frends.HTTP.Request/Frends.HTTP.Request/Request.cs @@ -61,7 +61,6 @@ CancellationToken cancellationToken ) { HttpClient httpClient = null; - HttpContent httpContent = null; try { @@ -70,12 +69,11 @@ CancellationToken cancellationToken httpClient = GetHttpClientForOptions(options); var headers = GetHeaderDictionary(input.Headers, options); - httpContent = GetContent(input, headers); using var responseMessage = await GetHttpRequestResponseAsync( httpClient, input.Method.ToString(), input.Url, - httpContent, + () => GetContent(input, headers), headers, options, cancellationToken) @@ -134,8 +132,6 @@ CancellationToken cancellationToken } finally { - httpContent?.Dispose(); - if (!options.CacheHttpClient) { httpClient?.Dispose(); @@ -270,13 +266,47 @@ private static string GetHttpClientCacheKey(Options options) private static async Task GetHttpRequestResponseAsync( HttpClient client, string method, string url, - HttpContent content, IDictionary headers, + Func contentFactory, IDictionary headers, Options options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var httpRequestMessage = new HttpRequestMessage(new HttpMethod(method), new Uri(url)); - httpRequestMessage.Content = content; + // A single HttpRequestMessage (and its HttpContent) can only be sent once. When the handler + // follows a redirect or transparently retries a request on a shared/cached HttpClient, the same + // message may be re-dispatched, causing "The request message was already sent. Cannot send the + // same request message multiple times." We build a fresh message per attempt and retry on that + // specific reuse exception so the request completes transparently. + const int maxAttempts = 3; + + for (var attempt = 1; ; attempt++) + { + var content = contentFactory(); + var httpRequestMessage = BuildRequestMessage(method, url, content, headers); + + try + { + return await SendRequestMessageAsync(client, httpRequestMessage, options, cancellationToken) + .ConfigureAwait(false); + } + catch (InvalidOperationException ex) when (attempt < maxAttempts && IsRequestAlreadySentException(ex)) + { + httpRequestMessage.Dispose(); + } + } + } + + private static bool IsRequestAlreadySentException(InvalidOperationException exception) + { + return exception.Message.Contains("already sent"); + } + + private static HttpRequestMessage BuildRequestMessage( + string method, string url, HttpContent content, IDictionary headers) + { + var httpRequestMessage = new HttpRequestMessage(new HttpMethod(method), new Uri(url)) + { + Content = content, + }; //Clear default headers content.Headers.Clear(); @@ -299,6 +329,13 @@ private static async Task GetHttpRequestResponseAsync( } } + return httpRequestMessage; + } + + private static async Task SendRequestMessageAsync( + HttpClient client, HttpRequestMessage httpRequestMessage, + Options options, CancellationToken cancellationToken) + { HttpResponseMessage httpResponseMessage; try {