Skip to content

NSUrlSessionHandler: Rewind request content to the start before sending #16341

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Feb 28, 2024
5 changes: 5 additions & 0 deletions src/Foundation/NSUrlSessionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,11 @@ async Task<NSUrlRequest> CreateRequest (HttpRequestMessage request)
};

if (stream != Stream.Null) {
// Rewind the stream to the beginning in case the HttpContent implementation
// will be accessed again (e.g. for retry/redirect) and it keeps its stream open behind the scenes.
if (stream.CanSeek)
stream.Seek (0, SeekOrigin.Begin);

// HttpContent.TryComputeLength is `protected internal` :-( but it's indirectly called by headers
var length = request.Content?.Headers?.ContentLength;
if (length.HasValue && (length <= MaxInputInMemory))
Expand Down
74 changes: 74 additions & 0 deletions tests/monotouch-test/System.Net.Http/MessageHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -758,5 +758,79 @@ public void GHIssue8344 ()
Assert.AreEqual (HttpStatusCode.Unauthorized, httpStatus, "Second status not ok");
}
}

class TestDelegateHandler : DelegatingHandler {
public int Iterations;
public HttpResponseMessage [] Responses;

public TestDelegateHandler (int iterations)
{
Responses = new HttpResponseMessage [iterations];
Iterations = iterations;
}

public bool IsCompleted (int iteration)
{
return Responses [iteration] is not null;
}

protected override async Task<HttpResponseMessage> SendAsync (HttpRequestMessage request, CancellationToken cancellationToken)
{
// test that we can perform a retry with the same request
for (var i = 0; i < Iterations; i++)
Responses [i] = await base.SendAsync (request, cancellationToken);
return Responses.Last ();
}
}

[TestCase]
public void GHIssue16339 ()
{
// test that we can perform two diff requests with the same managed HttpRequestMessage
var json = "{this:\"\", is:\"a\", test:2}";
var iterations = 2;
var bodies = new string [iterations];

var request = new HttpRequestMessage {
Method = HttpMethod.Post,
RequestUri = new (NetworkResources.Httpbin.PostUrl),
Content = new StringContent (json, Encoding.UTF8, "application/json")
};

using var delegatingHandler = new TestDelegateHandler (iterations) {
InnerHandler = new NSUrlSessionHandler (),
};

var done = TestRuntime.TryRunAsync (TimeSpan.FromSeconds (30), async () => {
using var client = new HttpClient (delegatingHandler);
var _ = await client.SendAsync (request);
for (var i = 0; i < iterations; i++) {
if (delegatingHandler.IsCompleted (i))
bodies [i] = await delegatingHandler.Responses [i].Content.ReadAsStringAsync ();
}
}, out var ex);

if (!done) { // timeouts happen in the bots due to dns issues, connection issues etc.. we do not want to fail
Assert.Inconclusive ("Request timedout.");
} else {
Assert.IsNull (ex, "Exception");

for (var i = 0; i < iterations; i++) {
var rsp = delegatingHandler.Responses [i];
Assert.IsTrue (delegatingHandler.IsCompleted (i), $"Completed #{i}");
Assert.IsTrue (rsp.IsSuccessStatusCode, $"IsSuccessStatusCode #{i}");
Assert.AreEqual ("OK", rsp.ReasonPhrase, $"ReasonPhrase #{i}");
Assert.AreEqual (HttpStatusCode.OK, rsp.StatusCode, $"StatusCode #{i}");

var body = bodies [i];
// Poor-man's json parser
var data = body.Split ('\n', '\r').Single (v => v.Contains ("\"data\": \""));
data = data.Trim ().Replace ("\"data\": \"", "").TrimEnd ('"', ',');
data = data.Replace ("\\\"", "\"");

Assert.AreEqual (json, data, $"Post data #{i}");
}
}
}
}
}