Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Frends.HTTP.Request/Frends.HTTP.Request.Tests/UnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,28 @@ public void RequestShouldThrowExceptionIfUrlEmpty()
Assert.That(ex.InnerException, Is.TypeOf<ArgumentNullException>());
}

[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<string>());
}

[TestMethod]
public void RequestShouldThrowExceptionIfOptionIsSet()
{
Expand Down
53 changes: 45 additions & 8 deletions Frends.HTTP.Request/Frends.HTTP.Request/Request.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ CancellationToken cancellationToken
)
{
HttpClient httpClient = null;
HttpContent httpContent = null;

try
{
Expand All @@ -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)
Expand Down Expand Up @@ -134,8 +132,6 @@ CancellationToken cancellationToken
}
finally
{
httpContent?.Dispose();

if (!options.CacheHttpClient)
{
httpClient?.Dispose();
Expand Down Expand Up @@ -270,13 +266,47 @@ private static string GetHttpClientCacheKey(Options options)

private static async Task<HttpResponseMessage> GetHttpRequestResponseAsync(
HttpClient client, string method, string url,
HttpContent content, IDictionary<string, string> headers,
Func<HttpContent> contentFactory, IDictionary<string, string> 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<string, string> headers)
{
var httpRequestMessage = new HttpRequestMessage(new HttpMethod(method), new Uri(url))
{
Content = content,
};

//Clear default headers
content.Headers.Clear();
Expand All @@ -299,6 +329,13 @@ private static async Task<HttpResponseMessage> GetHttpRequestResponseAsync(
}
}

return httpRequestMessage;
}

private static async Task<HttpResponseMessage> SendRequestMessageAsync(
HttpClient client, HttpRequestMessage httpRequestMessage,
Options options, CancellationToken cancellationToken)
{
HttpResponseMessage httpResponseMessage;
try
{
Expand Down
Loading