From 4fe01b30ea4c27dbc4605b3c36a084b0c0f4793c Mon Sep 17 00:00:00 2001 From: Steve Ward Date: Tue, 4 Dec 2018 23:40:17 +0000 Subject: [PATCH] Fixed deadlock when serializing content + added test coverage. --- Refit.Tests/MultipartTests.cs | 4 +- Refit.Tests/SerializedContentTests.cs | 114 ++++++++++++++++++++++++++ Refit/RequestBuilderImplementation.cs | 7 +- 3 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 Refit.Tests/SerializedContentTests.cs diff --git a/Refit.Tests/MultipartTests.cs b/Refit.Tests/MultipartTests.cs index 5e865d22b..2d851776c 100644 --- a/Refit.Tests/MultipartTests.cs +++ b/Refit.Tests/MultipartTests.cs @@ -409,7 +409,7 @@ public async Task MultipartUploadShouldWorkWithAnObject(Type contentSerializerTy { if (!(Activator.CreateInstance(contentSerializerType) is IContentSerializer serializer)) { - throw new ArithmeticException($"{contentSerializerType.FullName} does not implement {nameof(IContentSerializer)}"); + throw new ArgumentException($"{contentSerializerType.FullName} does not implement {nameof(IContentSerializer)}"); } var model1 = new ModelObject @@ -452,7 +452,7 @@ public async Task MultipartUploadShouldWorkWithObjects(Type contentSerializerTyp { if (!(Activator.CreateInstance(contentSerializerType) is IContentSerializer serializer)) { - throw new ArithmeticException($"{contentSerializerType.FullName} does not implement {nameof(IContentSerializer)}"); + throw new ArgumentException($"{contentSerializerType.FullName} does not implement {nameof(IContentSerializer)}"); } var model1 = new ModelObject diff --git a/Refit.Tests/SerializedContentTests.cs b/Refit.Tests/SerializedContentTests.cs new file mode 100644 index 000000000..d3c6a1223 --- /dev/null +++ b/Refit.Tests/SerializedContentTests.cs @@ -0,0 +1,114 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; +using System.Threading; + +namespace Refit.Tests +{ + public class SerializedContentTests + { + const string BaseAddress = "https://api/"; + + [Theory] + [InlineData(typeof(JsonContentSerializer))] + [InlineData(typeof(XmlContentSerializer))] + public async Task WhenARequestRequiresABodyThenItDoesNotDeadlock(Type contentSerializerType) + { + if (!(Activator.CreateInstance(contentSerializerType) is IContentSerializer serializer)) + { + throw new ArgumentException($"{contentSerializerType.FullName} does not implement {nameof(IContentSerializer)}"); + } + + var handler = new MockPushStreamContentHttpMessageHandler + { + Asserts = async content => new StringContent(await content.ReadAsStringAsync().ConfigureAwait(false)) + }; + + var settings = new RefitSettings() + { + HttpMessageHandlerFactory = () => handler, + ContentSerializer = serializer + }; + + var fixture = RestService.For(BaseAddress, settings); + + var fixtureTask = await RunTaskWithATimeLimit(fixture.CreateUser(new User())).ConfigureAwait(false); + Assert.True(fixtureTask.IsCompleted); + Assert.Equal(TaskStatus.RanToCompletion, fixtureTask.Status); + } + + [Theory] + [InlineData(typeof(JsonContentSerializer))] + [InlineData(typeof(XmlContentSerializer))] + public async Task WhenARequestRequiresABodyThenItIsSerialized(Type contentSerializerType) + { + if (!(Activator.CreateInstance(contentSerializerType) is IContentSerializer serializer)) + { + throw new ArgumentException($"{contentSerializerType.FullName} does not implement {nameof(IContentSerializer)}"); + } + + var model = new User + { + Name = "Wile E. Coyote", + CreatedAt = new DateTime(1949, 9, 16).ToString(), + Company = "ACME", + }; + + var handler = new MockPushStreamContentHttpMessageHandler + { + Asserts = async content => + { + var stringContent = new StringContent(await content.ReadAsStringAsync().ConfigureAwait(false)); + var user = await serializer.DeserializeAsync(content).ConfigureAwait(false); + Assert.NotSame(model, user); + Assert.Equal(model.Name, user.Name); + Assert.Equal(model.CreatedAt, user.CreatedAt); + Assert.Equal(model.Company, user.Company); + + // return some content so that the serializer doesn't complain + return stringContent; + } + }; + + var settings = new RefitSettings() + { + HttpMessageHandlerFactory = () => handler, + ContentSerializer = serializer + }; + + var fixture = RestService.For(BaseAddress, settings); + + var fixtureTask = await RunTaskWithATimeLimit(fixture.CreateUser(model)).ConfigureAwait(false); + + Assert.True(fixtureTask.IsCompleted); + } + + /// + /// Runs the task to completion or until the timeout occurs + /// + private static async Task> RunTaskWithATimeLimit(Task fixtureTask) + { + var circuitBreakerTask = Task.Delay(TimeSpan.FromSeconds(30)); + await Task.WhenAny(fixtureTask, circuitBreakerTask); + return fixtureTask; + } + + class MockPushStreamContentHttpMessageHandler : HttpMessageHandler + { + public Func> Asserts { get; set; } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var content = request.Content as PushStreamContent; + Assert.IsType(content); + Assert.NotNull(Asserts); + + var responseContent = await Asserts(content).ConfigureAwait(false); + + return new HttpResponseMessage(HttpStatusCode.OK) { Content = responseContent }; + } + } + } +} diff --git a/Refit/RequestBuilderImplementation.cs b/Refit/RequestBuilderImplementation.cs index c90eceba8..89b499059 100644 --- a/Refit/RequestBuilderImplementation.cs +++ b/Refit/RequestBuilderImplementation.cs @@ -475,7 +475,12 @@ Func> BuildRequestFactoryForMethod(RestMethod { case false: ret.Content = new PushStreamContent( - async (stream, _, __) => { await content.CopyToAsync(stream).ConfigureAwait(false); }, content.Headers.ContentType); + async (stream, _, __) => { + using (stream) + { + await content.CopyToAsync(stream).ConfigureAwait(false); + } + }, content.Headers.ContentType); break; case true: ret.Content = content;