From 832f312a35670fb878da47d02987ea5bbc60f5cf Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sun, 14 Apr 2019 23:05:58 +1200 Subject: [PATCH 01/13] HttpClient gRPC client --- Grpc.AspNetCore.sln | 7 + examples/Clients/Counter/Counter.csproj | 1 + examples/Clients/Counter/Program.cs | 27 +- examples/Clients/Greeter/Greeter.csproj | 1 + examples/Clients/Mailer/Program.cs | 5 +- examples/Server/Services/Greeter.cs | 3 - .../ClientAsyncStreamReader.cs | 65 ---- .../Grpc.NetCore.HttpClient.csproj | 6 +- .../GrpcClientFactory.cs | 5 +- .../HttpClientCallInvoker.cs | 177 ++-------- .../Internal/GrpcCall.cs | 246 ++++++++++++++ .../Internal/GrpcProtocolConstants.cs | 36 +++ .../Internal/GrpcProtocolHelpers.cs | 111 +++++++ .../Internal/HttpContentClientStreamWriter.cs | 112 +++++++ .../Internal/HttpContextClientStreamReader.cs | 89 +++++ .../Internal/ISystemClock.cs | 27 ++ .../Internal/PushStreamContent.cs | 53 +++ .../Internal/SerialiationHelpers.cs | 61 ++++ .../Internal/StreamExtensions.cs | 111 +++++++ .../Internal/SystemClock.cs | 29 ++ .../PipeClientStreamWriter.cs | 56 ---- src/Grpc.NetCore.HttpClient/PipeContent.cs | 79 ----- .../PipeWriterExtensions.cs | 63 ---- .../Properties/AssemblyInfo.cs | 6 + .../StreamExtensions.cs | 48 --- test/FunctionalTests/AuthorizationTests.cs | 1 + .../ClientStreamingMethodTests.cs | 6 +- test/FunctionalTests/CompressionTests.cs | 2 +- test/FunctionalTests/DeadlineTests.cs | 3 +- .../DuplexStreamingMethodTests.cs | 2 +- .../Grpc.AspNetCore.FunctionalTests.csproj | 1 + test/FunctionalTests/HttpContextTests.cs | 2 +- .../HttpResponseMessageExtensions.cs | 2 +- test/FunctionalTests/LifetimeTests.cs | 2 +- test/FunctionalTests/MaxMessageSizeTests.cs | 2 +- test/FunctionalTests/NestedTests.cs | 2 +- .../ServerStreamingMethodTests.cs | 1 + test/FunctionalTests/UnaryMethodTests.cs | 2 +- test/FunctionalTests/UnimplementedTests.cs | 1 + .../Grpc.AspNetCore.Server.Tests.csproj | 6 +- .../HttpContextStreamReaderTests.cs | 1 + .../AsyncClientStreamingCallTests.cs | 204 ++++++++++++ .../AsyncDuplexStreamingCallTests.cs | 166 ++++++++++ .../AsyncServerStreamingCallTests.cs | 157 +++++++++ .../AsyncUnaryCallTests.cs | 105 ++++++ .../DeadlineTests.cs | 133 ++++++++ .../GetTrailersTests.cs | 305 ++++++++++++++++++ .../Grpc.NetCore.HttpClient.Tests.csproj | 30 ++ .../GrpcClientFactoryTests.cs | 38 +++ .../HeadersTests.cs | 108 +++++++ .../Infrastructure/ResponseUtils.cs | 72 +++++ .../Infrastructure/TestHelpers.cs | 82 +++++ .../Infrastructure/TestHttpMessageHandler.cs | 50 +++ .../Proto/greet.proto | 35 ++ .../ResponseAsyncTests.cs | 112 +++++++ .../ResponseHeadersAsyncTests.cs | 186 +++++++++++ test/Shared/SyncPoint.cs | 2 +- test/Shared/SyncPointMemoryStream.cs | 2 +- .../TaskExtensions.cs | 2 +- 59 files changed, 2762 insertions(+), 487 deletions(-) delete mode 100644 src/Grpc.NetCore.HttpClient/ClientAsyncStreamReader.cs create mode 100644 src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs create mode 100644 src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolConstants.cs create mode 100644 src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolHelpers.cs create mode 100644 src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamWriter.cs create mode 100644 src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs create mode 100644 src/Grpc.NetCore.HttpClient/Internal/ISystemClock.cs create mode 100644 src/Grpc.NetCore.HttpClient/Internal/PushStreamContent.cs create mode 100644 src/Grpc.NetCore.HttpClient/Internal/SerialiationHelpers.cs create mode 100644 src/Grpc.NetCore.HttpClient/Internal/StreamExtensions.cs create mode 100644 src/Grpc.NetCore.HttpClient/Internal/SystemClock.cs delete mode 100644 src/Grpc.NetCore.HttpClient/PipeClientStreamWriter.cs delete mode 100644 src/Grpc.NetCore.HttpClient/PipeContent.cs delete mode 100644 src/Grpc.NetCore.HttpClient/PipeWriterExtensions.cs delete mode 100644 src/Grpc.NetCore.HttpClient/StreamExtensions.cs create mode 100644 test/Grpc.NetCore.HttpClient.Tests/AsyncClientStreamingCallTests.cs create mode 100644 test/Grpc.NetCore.HttpClient.Tests/AsyncDuplexStreamingCallTests.cs create mode 100644 test/Grpc.NetCore.HttpClient.Tests/AsyncServerStreamingCallTests.cs create mode 100644 test/Grpc.NetCore.HttpClient.Tests/AsyncUnaryCallTests.cs create mode 100644 test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs create mode 100644 test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs create mode 100644 test/Grpc.NetCore.HttpClient.Tests/Grpc.NetCore.HttpClient.Tests.csproj create mode 100644 test/Grpc.NetCore.HttpClient.Tests/GrpcClientFactoryTests.cs create mode 100644 test/Grpc.NetCore.HttpClient.Tests/HeadersTests.cs create mode 100644 test/Grpc.NetCore.HttpClient.Tests/Infrastructure/ResponseUtils.cs create mode 100644 test/Grpc.NetCore.HttpClient.Tests/Infrastructure/TestHelpers.cs create mode 100644 test/Grpc.NetCore.HttpClient.Tests/Infrastructure/TestHttpMessageHandler.cs create mode 100644 test/Grpc.NetCore.HttpClient.Tests/Proto/greet.proto create mode 100644 test/Grpc.NetCore.HttpClient.Tests/ResponseAsyncTests.cs create mode 100644 test/Grpc.NetCore.HttpClient.Tests/ResponseHeadersAsyncTests.cs rename test/{FunctionalTests/Infrastructure => Shared}/TaskExtensions.cs (98%) diff --git a/Grpc.AspNetCore.sln b/Grpc.AspNetCore.sln index 32cad6ca8..0f684fd08 100644 --- a/Grpc.AspNetCore.sln +++ b/Grpc.AspNetCore.sln @@ -110,6 +110,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Grpc.AspNetCore.Server.Refl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reflector", "examples\Clients\Reflector\Reflector.csproj", "{86AD33E9-2C07-45BD-B599-420C2618188D}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.NetCore.HttpClient.Tests", "test\Grpc.NetCore.HttpClient.Tests\Grpc.NetCore.HttpClient.Tests.csproj", "{2D26FB7A-72AD-41B9-9B06-44F50A8F8A49}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -188,6 +190,10 @@ Global {86AD33E9-2C07-45BD-B599-420C2618188D}.Debug|Any CPU.Build.0 = Debug|Any CPU {86AD33E9-2C07-45BD-B599-420C2618188D}.Release|Any CPU.ActiveCfg = Release|Any CPU {86AD33E9-2C07-45BD-B599-420C2618188D}.Release|Any CPU.Build.0 = Release|Any CPU + {2D26FB7A-72AD-41B9-9B06-44F50A8F8A49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D26FB7A-72AD-41B9-9B06-44F50A8F8A49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D26FB7A-72AD-41B9-9B06-44F50A8F8A49}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D26FB7A-72AD-41B9-9B06-44F50A8F8A49}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -221,6 +227,7 @@ Global {39320CA8-D8F0-45B6-B704-A04C16870226} = {310E5783-455A-4D09-A7AE-39DC2AB09504} {55813F20-1269-4B19-B03E-7E4A90148F92} = {8C62055F-8CD7-4859-9001-634D544DF2AE} {86AD33E9-2C07-45BD-B599-420C2618188D} = {F6E0F9D7-64E5-4C7B-A9BC-3C2AD687710B} + {2D26FB7A-72AD-41B9-9B06-44F50A8F8A49} = {CECC4AE8-9C4E-4727-939B-517CC2E58D65} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {CD5C2B19-49B4-480A-990C-36D98A719B07} diff --git a/examples/Clients/Counter/Counter.csproj b/examples/Clients/Counter/Counter.csproj index e18ad41a9..838b2a396 100644 --- a/examples/Clients/Counter/Counter.csproj +++ b/examples/Clients/Counter/Counter.csproj @@ -22,4 +22,5 @@ + diff --git a/examples/Clients/Counter/Program.cs b/examples/Clients/Counter/Program.cs index 0fd565f12..e6c4e8937 100644 --- a/examples/Clients/Counter/Program.cs +++ b/examples/Clients/Counter/Program.cs @@ -17,6 +17,7 @@ #endregion using System; +using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; using Common; @@ -36,9 +37,24 @@ static async Task Main(string[] args) var channel = new Channel("localhost:50051", credentials); var client = new Counter.CounterClient(channel); - var reply = client.IncrementCount(new Google.Protobuf.WellKnownTypes.Empty()); + await UnaryCallExample(client); + + await ClientStreamingCallExample(client); + + Console.WriteLine("Shutting down"); + await channel.ShutdownAsync(); + Console.WriteLine("Press any key to exit..."); + Console.ReadKey(); + } + + private static async Task UnaryCallExample(Counter.CounterClient client) + { + var reply = await client.IncrementCountAsync(new Google.Protobuf.WellKnownTypes.Empty()); Console.WriteLine("Count: " + reply.Count); + } + private static async Task ClientStreamingCallExample(Counter.CounterClient client) + { using (var call = client.AccumulateCount()) { for (int i = 0; i < 3; i++) @@ -50,13 +66,10 @@ static async Task Main(string[] args) } await call.RequestStream.CompleteAsync(); - Console.WriteLine($"Count: {(await call.ResponseAsync).Count}"); - } - Console.WriteLine("Shutting down"); - await channel.ShutdownAsync(); - Console.WriteLine("Press any key to exit..."); - Console.ReadKey(); + var response = await call; + Console.WriteLine($"Count: {response.Count}"); + } } } } diff --git a/examples/Clients/Greeter/Greeter.csproj b/examples/Clients/Greeter/Greeter.csproj index 20faebda9..fe67b0566 100644 --- a/examples/Clients/Greeter/Greeter.csproj +++ b/examples/Clients/Greeter/Greeter.csproj @@ -22,4 +22,5 @@ + diff --git a/examples/Clients/Mailer/Program.cs b/examples/Clients/Mailer/Program.cs index 0b454e061..7bcefb37e 100644 --- a/examples/Clients/Mailer/Program.cs +++ b/examples/Clients/Mailer/Program.cs @@ -43,7 +43,7 @@ static async Task Main(string[] args) await channel.ConnectAsync(); Console.WriteLine("Connected"); - Console.WriteLine("Press escape to exit. Press any other key to forward mail."); + Console.WriteLine("Press escape to disconnect. Press any other key to forward mail."); var client = new Mailer.MailerClient(channel); using (var mailbox = client.Mailbox(headers: new Metadata { new Metadata.Entry("mailbox-name", mailboxName) })) @@ -78,6 +78,9 @@ static async Task Main(string[] args) Console.WriteLine("Disconnecting"); await channel.ShutdownAsync(); + + Console.WriteLine("Disconnected. Press any key to exit."); + Console.ReadKey(); } private static string GetMailboxName(string[] args) diff --git a/examples/Server/Services/Greeter.cs b/examples/Server/Services/Greeter.cs index 7fa9a7984..695f9c26a 100644 --- a/examples/Server/Services/Greeter.cs +++ b/examples/Server/Services/Greeter.cs @@ -34,9 +34,6 @@ public GreeterService(ILoggerFactory loggerFactory) //Server side handler of the SayHello RPC public override Task SayHello(HelloRequest request, ServerCallContext context) { - var httpContext = context.GetHttpContext(); - _logger.LogInformation($"Connection id: {httpContext.Connection.Id}"); - _logger.LogInformation($"Sending hello to {request.Name}"); return Task.FromResult(new HelloReply { Message = "Hello " + request.Name }); } diff --git a/src/Grpc.NetCore.HttpClient/ClientAsyncStreamReader.cs b/src/Grpc.NetCore.HttpClient/ClientAsyncStreamReader.cs deleted file mode 100644 index a608f9b56..000000000 --- a/src/Grpc.NetCore.HttpClient/ClientAsyncStreamReader.cs +++ /dev/null @@ -1,65 +0,0 @@ -#region Copyright notice and license - -// Copyright 2019 The gRPC Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#endregion - -using System; -using System.IO; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Grpc.Core; - -namespace Grpc.NetCore.HttpClient -{ - internal class ClientAsyncStreamReader : IAsyncStreamReader - { - private Task _sendTask; - private Stream _responseStream; - private Func _deserializer; - - public ClientAsyncStreamReader(Task sendTask, Func deserializer) - { - _sendTask = sendTask; - _deserializer = deserializer; - } - - public TResponse Current { get; private set; } - - public void Dispose() - { - } - - public async Task MoveNext(CancellationToken cancellationToken) - { - try - { - if (_responseStream == null) - { - var responseMessage = await _sendTask; - _responseStream = await responseMessage.Content.ReadAsStreamAsync(); - } - - Current = _responseStream.ReadSingleMessage(_deserializer); - return true; - } - catch - { - return false; - } - } - } -} diff --git a/src/Grpc.NetCore.HttpClient/Grpc.NetCore.HttpClient.csproj b/src/Grpc.NetCore.HttpClient/Grpc.NetCore.HttpClient.csproj index 88ce1c68f..2b0c69f4d 100644 --- a/src/Grpc.NetCore.HttpClient/Grpc.NetCore.HttpClient.csproj +++ b/src/Grpc.NetCore.HttpClient/Grpc.NetCore.HttpClient.csproj @@ -12,15 +12,17 @@ - netstandard2.0 + netcoreapp3.0 8.0 true true + + + - diff --git a/src/Grpc.NetCore.HttpClient/GrpcClientFactory.cs b/src/Grpc.NetCore.HttpClient/GrpcClientFactory.cs index 394f0fc3e..3e8d36144 100644 --- a/src/Grpc.NetCore.HttpClient/GrpcClientFactory.cs +++ b/src/Grpc.NetCore.HttpClient/GrpcClientFactory.cs @@ -49,7 +49,10 @@ public static TClient Create(string baseAddress, X509Certificate certif handler.ClientCertificates.Add(certificate); } - return Cache.Instance.Activator(new HttpClientCallInvoker(handler, new Uri(baseAddress, UriKind.RelativeOrAbsolute))); + var httpClient = new System.Net.Http.HttpClient(handler); + httpClient.BaseAddress = new Uri(baseAddress, UriKind.RelativeOrAbsolute); + + return Cache.Instance.Activator(new HttpClientCallInvoker(httpClient)); } private class Cache diff --git a/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs b/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs index 63dbae751..94216ace6 100644 --- a/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs +++ b/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs @@ -21,6 +21,7 @@ using System.Threading; using System.Threading.Tasks; using Grpc.Core; +using Grpc.NetCore.HttpClient.Internal; namespace Grpc.NetCore.HttpClient { @@ -31,16 +32,8 @@ public class HttpClientCallInvoker : CallInvoker { private System.Net.Http.HttpClient _client; - /// - /// Initializes a new instance of the class. - /// - /// The primary client handler to use for gRPC requests. - /// The base address to use when making gRPC requests. - public HttpClientCallInvoker(HttpClientHandler handler, Uri baseAddress) - { - _client = new System.Net.Http.HttpClient(handler); - _client.BaseAddress = baseAddress; - } + // Override the current time for unit testing + internal ISystemClock Clock = SystemClock.Instance; /// /// Initializes a new instance of the class. @@ -80,22 +73,16 @@ public HttpClientCallInvoker(System.Net.Http.HttpClient client) /// public override AsyncClientStreamingCall AsyncClientStreamingCall(Method method, string host, CallOptions options) { - var pipeContent = new PipeContent(); - var message = new HttpRequestMessage(HttpMethod.Post, method.FullName); - message.Content = pipeContent; - message.Version = new Version(2, 0); - - var sendTask = SendRequestMessageAsync(() => Task.CompletedTask, _client, message); + var call = CreateGrpcCall(method, options); + call.SendClientStreaming(_client); return new AsyncClientStreamingCall( - requestStream: new PipeClientStreamWriter(pipeContent.PipeWriter, method.RequestMarshaller.Serializer, options.WriteOptions), - responseAsync: GetResponseAsync(sendTask, method.ResponseMarshaller.Deserializer), - responseHeadersAsync: GetResponseHeadersAsync(sendTask), - // Cannot implement due to trailers being unimplemented - getStatusFunc: () => new Status(), - // Cannot implement due to trailers being unimplemented - getTrailersFunc: () => new Metadata(), - disposeAction: () => { }); + requestStream: call.ClientStreamWriter, + responseAsync: call.GetResponseAsync(), + responseHeadersAsync: call.GetResponseHeadersAsync(), + getStatusFunc: call.GetStatus, + getTrailersFunc: call.GetTrailers, + disposeAction: call.Dispose); } /// @@ -105,22 +92,16 @@ public override AsyncClientStreamingCall AsyncClientStreami /// public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall(Method method, string host, CallOptions options) { - var pipeContent = new PipeContent(); - var message = new HttpRequestMessage(HttpMethod.Post, method.FullName); - message.Content = pipeContent; - message.Version = new Version(2, 0); - - var sendTask = SendRequestMessageAsync(() => Task.CompletedTask, _client, message); + var call = CreateGrpcCall(method, options); + call.SendDuplexStreaming(_client); return new AsyncDuplexStreamingCall( - requestStream: new PipeClientStreamWriter(pipeContent.PipeWriter, method.RequestMarshaller.Serializer, options.WriteOptions), - responseStream: new ClientAsyncStreamReader(sendTask, method.ResponseMarshaller.Deserializer), - responseHeadersAsync: GetResponseHeadersAsync(sendTask), - // Cannot implement due to trailers being unimplemented - getStatusFunc: () => new Status(), - // Cannot implement due to trailers being unimplemented - getTrailersFunc: () => new Metadata(), - disposeAction: () => { }); + requestStream: call.ClientStreamWriter, + responseStream: call.StreamReader, + responseHeadersAsync: call.GetResponseHeadersAsync(), + getStatusFunc: call.GetStatus, + getTrailersFunc: call.GetTrailers, + disposeAction: call.Dispose); } /// @@ -129,28 +110,15 @@ public override AsyncDuplexStreamingCall AsyncDuplexStreami /// public override AsyncServerStreamingCall AsyncServerStreamingCall(Method method, string host, CallOptions options, TRequest request) { - var content = new PipeContent(); - var message = new HttpRequestMessage(HttpMethod.Post, method.FullName); - message.Content = content; - message.Version = new Version(2, 0); - - // Write request body - var sendTask = SendRequestMessageAsync( - async () => - { - await content.PipeWriter.WriteMessageCoreAsync(method.RequestMarshaller.Serializer(request), true); - content.PipeWriter.Complete(); - }, - _client, message); + var call = CreateGrpcCall(method, options); + call.SendServerStreaming(_client); return new AsyncServerStreamingCall( - responseStream: new ClientAsyncStreamReader(sendTask, method.ResponseMarshaller.Deserializer), - responseHeadersAsync: GetResponseHeadersAsync(sendTask), - // Cannot implement due to trailers being unimplemented - getStatusFunc: () => new Status(), - // Cannot implement due to trailers being unimplemented - getTrailersFunc: () => new Metadata(), - disposeAction: () => { }); + responseStream: call.StreamReader, + responseHeadersAsync: call.GetResponseHeadersAsync(), + getStatusFunc: call.GetStatus, + getTrailersFunc: call.GetTrailers, + disposeAction: call.Dispose); } /// @@ -158,28 +126,15 @@ public override AsyncServerStreamingCall AsyncServerStreamingCall public override AsyncUnaryCall AsyncUnaryCall(Method method, string host, CallOptions options, TRequest request) { - var content = new PipeContent(); - var message = new HttpRequestMessage(HttpMethod.Post, method.FullName); - message.Content = content; - message.Version = new Version(2, 0); - - // Write request body - var sendTask = SendRequestMessageAsync( - async () => - { - await content.PipeWriter.WriteMessageCoreAsync(method.RequestMarshaller.Serializer(request), true); - content.PipeWriter.Complete(); - }, - _client, message); + var call = CreateGrpcCall(method, options); + call.SendUnary(_client, request); return new AsyncUnaryCall( - responseAsync: GetResponseAsync(sendTask, method.ResponseMarshaller.Deserializer), - responseHeadersAsync: GetResponseHeadersAsync(sendTask), - // Cannot implement due to trailers being unimplemented - getStatusFunc: () => new Status(), - // Cannot implement due to trailers being unimplemented - getTrailersFunc: () => new Metadata(), - disposeAction: () => { }); + responseAsync: call.GetResponseAsync(), + responseHeadersAsync: call.GetResponseHeadersAsync(), + getStatusFunc: call.GetStatus, + getTrailersFunc: call.GetTrailers, + disposeAction: call.Dispose); } /// @@ -190,71 +145,9 @@ public override TResponse BlockingUnaryCall(Method SendRequestMessageAsync(Func writeMessageTask, System.Net.Http.HttpClient client, HttpRequestMessage message) - { - await writeMessageTask(); - return await client.SendAsync(message, HttpCompletionOption.ResponseHeadersRead); - } - - private static async Task GetResponseAsync(Task sendTask, Func deserializer) - { - // We can't use pipes here since we can't control how much is read and response trailers causes InvalidOperationException - var response = await sendTask; - var responseStream = await response.Content.ReadAsStreamAsync(); - - return responseStream.ReadSingleMessage(deserializer); - } - - private static async Task GetResponseHeadersAsync(Task sendTask) + private GrpcCall CreateGrpcCall(Method method, CallOptions options) { - var response = await sendTask; - - var headers = new Metadata(); - - foreach (var header in response.Headers) - { - // ASP.NET Core includes pseudo headers in the set of request headers - // whereas, they are not in gRPC implementations. We will filter them - // out when we construct the list of headers on the context. - if (header.Key.StartsWith(":", StringComparison.Ordinal)) - { - continue; - } - else if (header.Key.EndsWith(Metadata.BinaryHeaderSuffix, StringComparison.OrdinalIgnoreCase)) - { - headers.Add(header.Key, ParseBinaryHeader(string.Join(",", header.Value))); - } - else - { - headers.Add(header.Key, string.Join(",", header.Value)); - } - } - return null; - } - - private static byte[] ParseBinaryHeader(string base64) - { - string decodable; - switch (base64.Length % 4) - { - case 0: - // base64 has the required padding - decodable = base64; - break; - case 2: - // 2 chars padding - decodable = base64 + "=="; - break; - case 3: - // 3 chars padding - decodable = base64 + "="; - break; - default: - // length%4 == 1 should be illegal - throw new FormatException("Invalid base64 header value"); - } - - return Convert.FromBase64String(decodable); + return new GrpcCall(method, options, Clock); } } } diff --git a/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs new file mode 100644 index 000000000..2cfaef566 --- /dev/null +++ b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs @@ -0,0 +1,246 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; + +namespace Grpc.NetCore.HttpClient.Internal +{ + internal class GrpcCall + { + private readonly CancellationTokenSource _callCts; + private readonly ISystemClock _clock; + + private HttpResponseMessage _httpResponse; + private Metadata _trailers; + + public bool Disposed { get; private set; } + public bool ResponseFinished { get; set; } + public CallOptions Options { get; } + public Method Method { get; } + public Task SendTask { get; private set; } + public HttpContentClientStreamWriter ClientStreamWriter { get; private set; } + + public HttpContextClientStreamReader StreamReader { get; private set; } + + public GrpcCall(Method method, CallOptions options, ISystemClock clock) + { + _callCts = new CancellationTokenSource(); + Method = method; + Options = options; + _clock = clock; + } + + public CancellationToken CancellationToken + { + get { return _callCts.Token; } + } + + public void SendUnary(System.Net.Http.HttpClient client, TRequest request) + { + HttpRequestMessage message = CreateHttpRequestMessage(); + + message.Content = new PushStreamContent( + (stream) => + { + return SerialiationHelpers.WriteMessage(stream, request, Method.RequestMarshaller.Serializer, Options.CancellationToken); + }, + GrpcProtocolConstants.GrpcContentTypeHeaderValue); + + SendCore(client, message); + } + + public void SendClientStreaming(System.Net.Http.HttpClient client) + { + var message = CreateHttpRequestMessage(); + ClientStreamWriter = CreateWriter(message); + + SendCore(client, message); + } + + public void SendServerStreaming(System.Net.Http.HttpClient client) + { + HttpRequestMessage message = CreateHttpRequestMessage(); + + SendCore(client, message); + + StreamReader = new HttpContextClientStreamReader(this); + } + + public void SendDuplexStreaming(System.Net.Http.HttpClient client) + { + var message = CreateHttpRequestMessage(); + ClientStreamWriter = CreateWriter(message); + + SendCore(client, message); + + StreamReader = new HttpContextClientStreamReader(this); + } + + public void Dispose() + { + if (!Disposed) + { + Disposed = true; + + _callCts.Cancel(); + _callCts.Dispose(); + _httpResponse?.Dispose(); + StreamReader?.Dispose(); + ClientStreamWriter?.Dispose(); + } + } + + private void SendCore(System.Net.Http.HttpClient client, HttpRequestMessage message) + { + SendTask = client.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, _callCts.Token); + } + + private HttpContentClientStreamWriter CreateWriter(HttpRequestMessage message) + { + TaskCompletionSource writeStreamTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + TaskCompletionSource completeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + message.Content = new PushStreamContent( + (stream) => + { + writeStreamTcs.SetResult(stream); + return completeTcs.Task; + }, + GrpcProtocolConstants.GrpcContentTypeHeaderValue); + + var writer = new HttpContentClientStreamWriter(this, writeStreamTcs.Task, completeTcs); + return writer; + } + + private HttpRequestMessage CreateHttpRequestMessage() + { + var message = new HttpRequestMessage(HttpMethod.Post, Method.FullName); + message.Version = new Version(2, 0); + + if (Options.Headers != null && Options.Headers.Count > 0) + { + foreach (var entry in Options.Headers) + { + // Deadline is set via CallOptions.Deadline + if (entry.Key == GrpcProtocolConstants.TimeoutHeader) + { + continue; + } + + var value = entry.IsBinary ? Convert.ToBase64String(entry.ValueBytes) : entry.Value; + message.Headers.Add(entry.Key, value); + } + } + + if (Options.Deadline != null && Options.Deadline != DateTime.MaxValue) + { + var deadline = Options.Deadline.Value - _clock.UtcNow; + + // JamesNK(todo) - Replicate C core's logic for formatting grpc-timeout + message.Headers.Add(GrpcProtocolConstants.TimeoutHeader, Convert.ToInt64(deadline.TotalMilliseconds) + "m"); + } + + return message; + } + + public void EnsureNotDisposed() + { + if (Disposed) + { + throw new ObjectDisposedException(nameof(GrpcCall)); + } + } + + public async Task GetResponseAsync() + { + _httpResponse = await SendTask.ConfigureAwait(false); + var responseStream = await _httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); + + var message = await responseStream.ReadSingleMessageAsync(Method.ResponseMarshaller.Deserializer, _callCts.Token).ConfigureAwait(false); + ResponseFinished = true; + + // The task of this method is cached so there is no need to cache the message here + return message; + } + + public async Task GetResponseHeadersAsync() + { + _httpResponse = await SendTask.ConfigureAwait(false); + + // The task of this method is cached so there is no need to cache the headers here + return GrpcProtocolHelpers.BuildMetadata(_httpResponse.Headers); + } + + public Status GetStatus() + { + ValidateTrailersAvailable(); + + var grpcStatus = SendTask.Result.TrailingHeaders.GetValues(GrpcProtocolConstants.StatusTrailer).FirstOrDefault(); + var grpcMessage = SendTask.Result.TrailingHeaders.GetValues(GrpcProtocolConstants.MessageTrailer).FirstOrDefault(); + + int statusValue; + if (grpcStatus == null) + { + throw new InvalidOperationException("Response did not have a grpc-status trailer."); + } + else if (!int.TryParse(grpcStatus, out statusValue)) + { + throw new InvalidOperationException("Unexpected grpc-status value: " + grpcStatus); + } + + return new Status((StatusCode)statusValue, grpcMessage); + } + + public Metadata GetTrailers() + { + if (_trailers == null) + { + ValidateTrailersAvailable(); + + _trailers = GrpcProtocolHelpers.BuildMetadata(SendTask.Result.TrailingHeaders); + } + + return _trailers; + } + + private void ValidateTrailersAvailable() + { + // Async call could have been disposed + EnsureNotDisposed(); + + // HttpClient.SendAsync could have failed + if (SendTask.IsFaulted) + { + throw new InvalidOperationException("Can't get the call trailers because an error occured when making the request.", SendTask.Exception); + } + + // Response could still be in progress + if (!ResponseFinished || !SendTask.IsCompletedSuccessfully) + { + throw new InvalidOperationException("Can't get the call trailers because the call is not complete."); + } + } + } +} diff --git a/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolConstants.cs b/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolConstants.cs new file mode 100644 index 000000000..d786d2322 --- /dev/null +++ b/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolConstants.cs @@ -0,0 +1,36 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System.Net.Http.Headers; + +namespace Grpc.NetCore.HttpClient.Internal +{ + internal static class GrpcProtocolConstants + { + internal const string GrpcContentType = "application/grpc"; + internal static readonly MediaTypeHeaderValue GrpcContentTypeHeaderValue = new MediaTypeHeaderValue("application/grpc"); + + internal const string TimeoutHeader = "grpc-timeout"; + internal const string MessageEncodingHeader = "grpc-encoding"; + + internal const string StatusTrailer = "grpc-status"; + internal const string MessageTrailer = "grpc-message"; + + internal const string MessageAcceptEncodingHeader = "grpc-accept-encoding"; + } +} diff --git a/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolHelpers.cs b/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolHelpers.cs new file mode 100644 index 000000000..1a7b2116a --- /dev/null +++ b/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolHelpers.cs @@ -0,0 +1,111 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Net.Http.Headers; +using Grpc.Core; + +namespace Grpc.NetCore.HttpClient.Internal +{ + internal static class GrpcProtocolHelpers + { + public static bool IsGrpcContentType(string contentType) + { + if (contentType == null) + { + return false; + } + + if (!contentType.StartsWith(GrpcProtocolConstants.GrpcContentType, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (contentType.Length == GrpcProtocolConstants.GrpcContentType.Length) + { + // Exact match + return true; + } + + // Support variations on the content-type (e.g. +proto, +json) + char nextChar = contentType[GrpcProtocolConstants.GrpcContentType.Length]; + if (nextChar == ';') + { + return true; + } + if (nextChar == '+') + { + // Accept any message format. Marshaller could be set to support third-party formats + return true; + } + + return false; + } + + public static byte[] ParseBinaryHeader(string base64) + { + string decodable; + switch (base64.Length % 4) + { + case 0: + // base64 has the required padding + decodable = base64; + break; + case 2: + // 2 chars padding + decodable = base64 + "=="; + break; + case 3: + // 3 chars padding + decodable = base64 + "="; + break; + default: + // length%4 == 1 should be illegal + throw new FormatException("Invalid base64 header value"); + } + + return Convert.FromBase64String(decodable); + } + + public static Metadata BuildMetadata(HttpResponseHeaders responseHeaders) + { + var headers = new Metadata(); + + foreach (var header in responseHeaders) + { + // ASP.NET Core includes pseudo headers in the set of request headers + // whereas, they are not in gRPC implementations. We will filter them + // out when we construct the list of headers on the context. + if (header.Key.StartsWith(":", StringComparison.Ordinal)) + { + continue; + } + else if (header.Key.EndsWith(Metadata.BinaryHeaderSuffix, StringComparison.OrdinalIgnoreCase)) + { + headers.Add(header.Key, GrpcProtocolHelpers.ParseBinaryHeader(string.Join(",", header.Value))); + } + else + { + headers.Add(header.Key, string.Join(",", header.Value)); + } + } + + return headers; + } + } +} diff --git a/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamWriter.cs b/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamWriter.cs new file mode 100644 index 000000000..4c954e97e --- /dev/null +++ b/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamWriter.cs @@ -0,0 +1,112 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; + +namespace Grpc.NetCore.HttpClient.Internal +{ + internal class HttpContentClientStreamWriter : IClientStreamWriter + { + private readonly GrpcCall _call; + private readonly Task _writeStreamTask; + private readonly TaskCompletionSource _completeTcs; + private readonly object _writeLock; + private Task _writeTask; + + public HttpContentClientStreamWriter(GrpcCall call, Task writeStreamTask, TaskCompletionSource completeTcs) + { + _call = call; + _writeStreamTask = writeStreamTask; + _completeTcs = completeTcs; + _writeLock = new object(); + WriteOptions = _call.Options.WriteOptions; + } + + public WriteOptions WriteOptions { get; set; } + + public Task CompleteAsync() + { + lock (_writeLock) + { + // Pending writes need to be awaited first + if (IsWriteInProgress) + { + return Task.FromException(new InvalidOperationException("Cannot complete client stream writer because the previous write is in progress.")); + } + + // Notify that the client stream is complete + _completeTcs.TrySetResult(true); + } + + return Task.CompletedTask; + } + + public Task WriteAsync(TRequest message) + { + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + + lock (_writeLock) + { + // CompleteAsync has already been called + if (_completeTcs.Task.IsCompletedSuccessfully) + { + return Task.FromException(new InvalidOperationException("Cannot write message because the client stream writer is complete.")); + } + + // Pending writes need to be awaited first + if (IsWriteInProgress) + { + return Task.FromException(new InvalidOperationException("Cannot write message because the previous write is in progress.")); + } + + // Save write task to track whether it is complete + _writeTask = WriteAsyncCore(message); + } + + return _writeTask; + } + + public void Dispose() + { + } + + private async Task WriteAsyncCore(TRequest message) + { + // Wait until the client stream has started + var writeStream = await _writeStreamTask.ConfigureAwait(false); + + await SerialiationHelpers.WriteMessage(writeStream, message, _call.Method.RequestMarshaller.Serializer, _call.CancellationToken).ConfigureAwait(false); + } + + private bool IsWriteInProgress + { + get + { + var writeTask = _writeTask; + return writeTask != null && !writeTask.IsCompleted; + } + } + } +} diff --git a/src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs b/src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs new file mode 100644 index 000000000..8c8488800 --- /dev/null +++ b/src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs @@ -0,0 +1,89 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; + +namespace Grpc.NetCore.HttpClient.Internal +{ + internal class HttpContextClientStreamReader : IAsyncStreamReader + { + private readonly GrpcCall _call; + private Stream _responseStream; + + public HttpContextClientStreamReader(GrpcCall call) + { + _call = call; + } + + public TResponse Current { get; private set; } + + public void Dispose() + { + } + + public async Task MoveNext(CancellationToken cancellationToken) + { + // HTTP response has finished + if (_call.ResponseFinished) + { + return false; + } + + // User could have disposed call + _call.EnsureNotDisposed(); + + // Linking tokens is expensive. Only create a linked token is passed in cancellation token requires it + CancellationTokenSource cts = null; + if (cancellationToken.CanBeCanceled) + { + cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _call.CancellationToken); + cancellationToken = cts.Token; + } + else + { + cancellationToken = _call.CancellationToken; + } + + cancellationToken.ThrowIfCancellationRequested(); + + if (_responseStream == null) + { + var responseMessage = await _call.SendTask.ConfigureAwait(false); + _responseStream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false); + } + + using (cts) + { + Current = await _responseStream.ReadStreamedMessageAsync(_call.Method.ResponseMarshaller.Deserializer, cancellationToken).ConfigureAwait(false); + if (Current == null) + { + // No more content in response so mark as finished + _call.ResponseFinished = true; + return false; + } + + return true; + } + } + } +} diff --git a/src/Grpc.NetCore.HttpClient/Internal/ISystemClock.cs b/src/Grpc.NetCore.HttpClient/Internal/ISystemClock.cs new file mode 100644 index 000000000..016c52ee0 --- /dev/null +++ b/src/Grpc.NetCore.HttpClient/Internal/ISystemClock.cs @@ -0,0 +1,27 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; + +namespace Grpc.NetCore.HttpClient.Internal +{ + internal interface ISystemClock + { + DateTime UtcNow { get; } + } +} diff --git a/src/Grpc.NetCore.HttpClient/Internal/PushStreamContent.cs b/src/Grpc.NetCore.HttpClient/Internal/PushStreamContent.cs new file mode 100644 index 000000000..51e28f87e --- /dev/null +++ b/src/Grpc.NetCore.HttpClient/Internal/PushStreamContent.cs @@ -0,0 +1,53 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; + +namespace Grpc.NetCore.HttpClient.Internal +{ + internal class PushStreamContent : HttpContent + { + private readonly Func _onStreamAvailable; + + public PushStreamContent(Func onStreamAvailable, MediaTypeHeaderValue mediaType) + { + _onStreamAvailable = onStreamAvailable; + Headers.ContentType = mediaType; + } + + protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + return _onStreamAvailable(stream); + } + + protected override bool TryComputeLength(out long length) + { + // We can't know the length of the content being pushed to the output stream. + length = -1; + return false; + } + + // Hacky. ReadAsStreamAsync does not complete until SerializeToStreamAsync finishes + internal Task PushComplete => ReadAsStreamAsync(); + } +} \ No newline at end of file diff --git a/src/Grpc.NetCore.HttpClient/Internal/SerialiationHelpers.cs b/src/Grpc.NetCore.HttpClient/Internal/SerialiationHelpers.cs new file mode 100644 index 000000000..2c7826cde --- /dev/null +++ b/src/Grpc.NetCore.HttpClient/Internal/SerialiationHelpers.cs @@ -0,0 +1,61 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Buffers.Binary; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Grpc.NetCore.HttpClient.Internal +{ + internal static class SerialiationHelpers + { + public static async Task WriteMessage(Stream stream, TMessage message, Func serializer, CancellationToken cancellationToken) + { + var data = serializer(message); + + await WriteHeaderAsync(stream, data.Length, false, cancellationToken).ConfigureAwait(false); + await stream.WriteAsync(data, cancellationToken).ConfigureAwait(false); + } + + private const int MessageDelimiterSize = 4; // how many bytes it takes to encode "Message-Length" + private const int HeaderSize = MessageDelimiterSize + 1; // message length + compression flag + + public static Task WriteHeaderAsync(Stream stream, int length, bool compress, CancellationToken cancellationToken) + { + var headerData = new byte[HeaderSize]; + + // Compression flag + headerData[0] = compress ? (byte)1 : (byte)0; + + // Message length + EncodeMessageLength(length, headerData.AsSpan(1)); + + return stream.WriteAsync(headerData, 0, headerData.Length, cancellationToken); + } + + private static void EncodeMessageLength(int messageLength, Span destination) + { + Debug.Assert(destination.Length >= MessageDelimiterSize, "Buffer too small to encode message length."); + + BinaryPrimitives.WriteUInt32BigEndian(destination, (uint)messageLength); + } + } +} diff --git a/src/Grpc.NetCore.HttpClient/Internal/StreamExtensions.cs b/src/Grpc.NetCore.HttpClient/Internal/StreamExtensions.cs new file mode 100644 index 000000000..72d172508 --- /dev/null +++ b/src/Grpc.NetCore.HttpClient/Internal/StreamExtensions.cs @@ -0,0 +1,111 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Buffers.Binary; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Grpc.NetCore.HttpClient +{ + internal static class StreamExtensions + { + public static Task ReadSingleMessageAsync(this Stream responseStream, Func deserializer, CancellationToken cancellationToken) + { + return responseStream.ReadMessageCoreAsync(deserializer, cancellationToken, true); + } + + public static Task ReadStreamedMessageAsync(this Stream responseStream, Func deserializer, CancellationToken cancellationToken) + { + return responseStream.ReadMessageCoreAsync(deserializer, cancellationToken, false); + } + + private static async Task ReadMessageCoreAsync(this Stream responseStream, Func deserializer, CancellationToken cancellationToken, bool singleMessage) + { + cancellationToken.ThrowIfCancellationRequested(); + + var header = new byte[5]; + + int read; + var received = 0; + while ((read = await responseStream.ReadAsync(header, received, header.Length - received, cancellationToken).ConfigureAwait(false)) > 0) + { + received += read; + + if (received == header.Length) + { + break; + } + } + + if (received < header.Length) + { + if (received == 0 && !singleMessage) + { + return default; + } + + throw new InvalidDataException("Unexpected end of content while reading the message header."); + } + + var length = BinaryPrimitives.ReadUInt32BigEndian(header.AsSpan(1)); + if (length > int.MaxValue) + { + throw new InvalidDataException("Message too large."); + } + + byte[] messageData; + if (length > 0) + { + received = 0; + messageData = new byte[length]; + while ((read = await responseStream.ReadAsync(messageData, received, messageData.Length - received, cancellationToken).ConfigureAwait(false)) > 0) + { + received += read; + + if (received == messageData.Length) + { + break; + } + } + } + else + { + messageData = Array.Empty(); + } + + cancellationToken.ThrowIfCancellationRequested(); + + var message = deserializer(messageData); + + if (singleMessage) + { + // Check that there is no additional content in the stream for a single message + // There is no ReadByteAsync on stream. Reuse header array with ReadAsync, we don't need it anymore + if (await responseStream.ReadAsync(header, 0, 1).ConfigureAwait(false) > 0) + { + throw new InvalidDataException("Unexpected data after finished reading message."); + } + } + + return message; + } + + } +} diff --git a/src/Grpc.NetCore.HttpClient/Internal/SystemClock.cs b/src/Grpc.NetCore.HttpClient/Internal/SystemClock.cs new file mode 100644 index 000000000..87321c263 --- /dev/null +++ b/src/Grpc.NetCore.HttpClient/Internal/SystemClock.cs @@ -0,0 +1,29 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; + +namespace Grpc.NetCore.HttpClient.Internal +{ + internal class SystemClock : ISystemClock + { + public static readonly SystemClock Instance = new SystemClock(); + + public DateTime UtcNow => DateTime.UtcNow; + } +} diff --git a/src/Grpc.NetCore.HttpClient/PipeClientStreamWriter.cs b/src/Grpc.NetCore.HttpClient/PipeClientStreamWriter.cs deleted file mode 100644 index 18605f46e..000000000 --- a/src/Grpc.NetCore.HttpClient/PipeClientStreamWriter.cs +++ /dev/null @@ -1,56 +0,0 @@ -#region Copyright notice and license - -// Copyright 2019 The gRPC Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#endregion - -using System; -using System.IO.Pipelines; -using System.Threading.Tasks; -using Grpc.Core; - -namespace Grpc.NetCore.HttpClient -{ - internal class PipeClientStreamWriter : IClientStreamWriter - { - private readonly PipeWriter _writer; - private readonly Func _serializer; - - public PipeClientStreamWriter(PipeWriter Writer, Func serializer, WriteOptions options) - { - _writer = Writer; - _serializer = serializer; - WriteOptions = options; - } - - public WriteOptions WriteOptions { get; set; } - - public Task CompleteAsync() - { - _writer.Complete(); - return Task.CompletedTask; - } - - public Task WriteAsync(TRequest message) - { - if (message == null) - { - throw new ArgumentNullException(nameof(message)); - } - - return _writer.WriteMessageCoreAsync(_serializer(message), flush: true); - } - } -} diff --git a/src/Grpc.NetCore.HttpClient/PipeContent.cs b/src/Grpc.NetCore.HttpClient/PipeContent.cs deleted file mode 100644 index 6982828f0..000000000 --- a/src/Grpc.NetCore.HttpClient/PipeContent.cs +++ /dev/null @@ -1,79 +0,0 @@ -#region Copyright notice and license - -// Copyright 2019 The gRPC Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#endregion - -using System.Buffers; -using System.IO; -using System.IO.Pipelines; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; - -namespace Grpc.NetCore.HttpClient -{ - internal class PipeContent : HttpContent - { - private Pipe _pipe = new Pipe(); - - public PipeWriter PipeWriter => _pipe.Writer; - private PipeReader PipeReader => _pipe.Reader; - - public PipeContent() - { - Headers.ContentType = new MediaTypeHeaderValue("application/grpc"); - } - - protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) - { - while (true) - { - var result = await PipeReader.ReadAsync(); - var buffer = result.Buffer; - - try - { - if (result.IsCanceled) - { - throw new TaskCanceledException(); - } - - if (!buffer.IsEmpty) - { - var data = buffer.ToArray(); - stream.Write(data, 0, data.Length); - } - - if (result.IsCompleted) - { - break; - } - } - finally - { - PipeReader.AdvanceTo(buffer.End); - } - } - } - - protected override bool TryComputeLength(out long length) - { - length = 0; - return false; - } - } -} diff --git a/src/Grpc.NetCore.HttpClient/PipeWriterExtensions.cs b/src/Grpc.NetCore.HttpClient/PipeWriterExtensions.cs deleted file mode 100644 index 64867f326..000000000 --- a/src/Grpc.NetCore.HttpClient/PipeWriterExtensions.cs +++ /dev/null @@ -1,63 +0,0 @@ -#region Copyright notice and license - -// Copyright 2019 The gRPC Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#endregion - -using System.Buffers; -using System.Buffers.Binary; -using System.IO.Pipelines; -using System.Threading.Tasks; - -namespace Grpc.NetCore.HttpClient -{ - internal static class PipeWriterExtensions - { - private const int MessageDelimiterSize = 4; // how many bytes it takes to encode "Message-Length" - private const int HeaderSize = MessageDelimiterSize + 1; // message length + compression flag - - public static Task WriteMessageCoreAsync(this PipeWriter pipeWriter, byte[] messageData, bool flush) - { - WriteHeader(pipeWriter, messageData.Length); - pipeWriter.Write(messageData); - - if (flush) - { - var valueTask = pipeWriter.FlushAsync(); - - if (valueTask.IsCompletedSuccessfully) - { - // We do this to reset the underlying value task (which happens in GetResult()) - valueTask.GetAwaiter().GetResult(); - return Task.CompletedTask; - } - - return valueTask.AsTask(); - } - - return Task.CompletedTask; - } - - private static void WriteHeader(PipeWriter pipeWriter, int length) - { - var headerData = pipeWriter.GetSpan(HeaderSize); - // Messages are currently always uncompressed - headerData[0] = 0; - BinaryPrimitives.WriteUInt32BigEndian(headerData.Slice(1), (uint)length); - - pipeWriter.Advance(HeaderSize); - } - } -} diff --git a/src/Grpc.NetCore.HttpClient/Properties/AssemblyInfo.cs b/src/Grpc.NetCore.HttpClient/Properties/AssemblyInfo.cs index 9e89fd1e0..c81c37157 100644 --- a/src/Grpc.NetCore.HttpClient/Properties/AssemblyInfo.cs +++ b/src/Grpc.NetCore.HttpClient/Properties/AssemblyInfo.cs @@ -24,6 +24,12 @@ "27fc95aff3dc604a6971417453f9483c7b5e836756d5b271bf8f2403fe186e31956148c03d804487cf642f8cc0" + "71394ee9672dfe5b55ea0f95dfd5a7f77d22c962ccf51320d3")] +[assembly: InternalsVisibleTo("Grpc.NetCore.HttpClient.Tests,PublicKey=" + + "00240000048000009400000006020000002400005253413100040000010001002f5797a92c6fcde81bd4098f43" + + "0442bb8e12768722de0b0cb1b15e955b32a11352740ee59f2c94c48edc8e177d1052536b8ac651bce11ce5da3a" + + "27fc95aff3dc604a6971417453f9483c7b5e836756d5b271bf8f2403fe186e31956148c03d804487cf642f8cc0" + + "71394ee9672dfe5b55ea0f95dfd5a7f77d22c962ccf51320d3")] + // For Moq. This assembly needs access to internal types via InternalVisibleTo to be able to mock them [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602" + "000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02ba" + diff --git a/src/Grpc.NetCore.HttpClient/StreamExtensions.cs b/src/Grpc.NetCore.HttpClient/StreamExtensions.cs deleted file mode 100644 index e6f35daef..000000000 --- a/src/Grpc.NetCore.HttpClient/StreamExtensions.cs +++ /dev/null @@ -1,48 +0,0 @@ -#region Copyright notice and license - -// Copyright 2019 The gRPC Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#endregion - -using System; -using System.Buffers.Binary; -using System.IO; - -namespace Grpc.NetCore.HttpClient -{ - internal static class StreamExtensions - { - public static TResponse ReadSingleMessage(this Stream responseStream, Func deserializer) - { - if (responseStream.ReadByte() != 0) - { - throw new InvalidOperationException("Compressed response not yet supported"); - } - - var lengthBytes = new byte[4]; - responseStream.Read(lengthBytes, 0, 4); - var length = BinaryPrimitives.ReadUInt32BigEndian(lengthBytes); - if (length > int.MaxValue) - { - throw new InvalidOperationException("message too large"); - } - - var responseBytes = new byte[length]; - responseStream.Read(responseBytes, 0, (int)length); - - return deserializer(responseBytes); - } - } -} diff --git a/test/FunctionalTests/AuthorizationTests.cs b/test/FunctionalTests/AuthorizationTests.cs index be65d5423..d279ec1d6 100644 --- a/test/FunctionalTests/AuthorizationTests.cs +++ b/test/FunctionalTests/AuthorizationTests.cs @@ -25,6 +25,7 @@ using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; using Grpc.Core; +using Grpc.Tests; using NUnit.Framework; namespace Grpc.AspNetCore.FunctionalTests diff --git a/test/FunctionalTests/ClientStreamingMethodTests.cs b/test/FunctionalTests/ClientStreamingMethodTests.cs index a505967a9..efc998188 100644 --- a/test/FunctionalTests/ClientStreamingMethodTests.cs +++ b/test/FunctionalTests/ClientStreamingMethodTests.cs @@ -27,7 +27,7 @@ using Google.Protobuf.WellKnownTypes; using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; -using Grpc.AspNetCore.Server.Tests; +using Grpc.Tests; using Grpc.Core; using NUnit.Framework; @@ -157,7 +157,7 @@ static async Task AccumulateCount(IAsyncStreamReader AccumulateCount(IAsyncStreamReader(); + var reply = await response.GetSuccessfulGrpcMessageAsync().DefaultTimeout(); Assert.AreEqual(3, reply.Count); Assert.AreEqual(StatusCode.OK.ToTrailerString(), Fixture.TrailersContainer.Trailers[GrpcProtocolConstants.StatusTrailer].Single()); diff --git a/test/FunctionalTests/CompressionTests.cs b/test/FunctionalTests/CompressionTests.cs index c1e479a43..ec5d51924 100644 --- a/test/FunctionalTests/CompressionTests.cs +++ b/test/FunctionalTests/CompressionTests.cs @@ -29,7 +29,7 @@ using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Compression; using Grpc.AspNetCore.Server.Internal; -using Grpc.AspNetCore.Server.Tests; +using Grpc.Tests; using Grpc.Core; using NUnit.Framework; diff --git a/test/FunctionalTests/DeadlineTests.cs b/test/FunctionalTests/DeadlineTests.cs index 5ad8e8780..7eb8f4c96 100644 --- a/test/FunctionalTests/DeadlineTests.cs +++ b/test/FunctionalTests/DeadlineTests.cs @@ -26,6 +26,7 @@ using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; using Grpc.Core; +using Grpc.Tests; using NUnit.Framework; namespace Grpc.AspNetCore.FunctionalTests @@ -49,7 +50,7 @@ public Task WriteUntilDeadline_SuccessResponsesStreamed_Deadline() => } // Ensure deadline timer has run - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); context.CancellationToken.Register(() => tcs.SetResult(null)); await tcs.Task; }); diff --git a/test/FunctionalTests/DuplexStreamingMethodTests.cs b/test/FunctionalTests/DuplexStreamingMethodTests.cs index d44a90fff..4268cced0 100644 --- a/test/FunctionalTests/DuplexStreamingMethodTests.cs +++ b/test/FunctionalTests/DuplexStreamingMethodTests.cs @@ -26,8 +26,8 @@ using FunctionalTestsWebsite.Services; using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; -using Grpc.AspNetCore.Server.Tests; using Grpc.Core; +using Grpc.Tests; using NUnit.Framework; namespace Grpc.AspNetCore.FunctionalTests diff --git a/test/FunctionalTests/Grpc.AspNetCore.FunctionalTests.csproj b/test/FunctionalTests/Grpc.AspNetCore.FunctionalTests.csproj index 7c30c07d0..099da3d6a 100644 --- a/test/FunctionalTests/Grpc.AspNetCore.FunctionalTests.csproj +++ b/test/FunctionalTests/Grpc.AspNetCore.FunctionalTests.csproj @@ -11,6 +11,7 @@ + diff --git a/test/FunctionalTests/HttpContextTests.cs b/test/FunctionalTests/HttpContextTests.cs index 18ae0b944..4ccfd024b 100644 --- a/test/FunctionalTests/HttpContextTests.cs +++ b/test/FunctionalTests/HttpContextTests.cs @@ -22,7 +22,7 @@ using Greet; using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; -using Grpc.AspNetCore.Server.Tests; +using Grpc.Tests; using Grpc.Core; using NUnit.Framework; diff --git a/test/FunctionalTests/Infrastructure/HttpResponseMessageExtensions.cs b/test/FunctionalTests/Infrastructure/HttpResponseMessageExtensions.cs index 3660d4778..8b02cef79 100644 --- a/test/FunctionalTests/Infrastructure/HttpResponseMessageExtensions.cs +++ b/test/FunctionalTests/Infrastructure/HttpResponseMessageExtensions.cs @@ -16,11 +16,11 @@ #endregion -using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; using Google.Protobuf; +using Grpc.Tests; using NUnit.Framework; namespace Grpc.AspNetCore.FunctionalTests.Infrastructure diff --git a/test/FunctionalTests/LifetimeTests.cs b/test/FunctionalTests/LifetimeTests.cs index afa949125..768fa76e7 100644 --- a/test/FunctionalTests/LifetimeTests.cs +++ b/test/FunctionalTests/LifetimeTests.cs @@ -25,7 +25,7 @@ using Google.Protobuf.WellKnownTypes; using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; -using Grpc.AspNetCore.Server.Tests; +using Grpc.Tests; using Grpc.Core; using Lifetime; using NUnit.Framework; diff --git a/test/FunctionalTests/MaxMessageSizeTests.cs b/test/FunctionalTests/MaxMessageSizeTests.cs index b32a95254..a3253f94f 100644 --- a/test/FunctionalTests/MaxMessageSizeTests.cs +++ b/test/FunctionalTests/MaxMessageSizeTests.cs @@ -23,7 +23,7 @@ using Greet; using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; -using Grpc.AspNetCore.Server.Tests; +using Grpc.Tests; using Grpc.Core; using NUnit.Framework; diff --git a/test/FunctionalTests/NestedTests.cs b/test/FunctionalTests/NestedTests.cs index 0c3601b5a..750a9b4fd 100644 --- a/test/FunctionalTests/NestedTests.cs +++ b/test/FunctionalTests/NestedTests.cs @@ -24,7 +24,7 @@ using System.Threading.Tasks; using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; -using Grpc.AspNetCore.Server.Tests; +using Grpc.Tests; using Grpc.Core; using Nested; using NUnit.Framework; diff --git a/test/FunctionalTests/ServerStreamingMethodTests.cs b/test/FunctionalTests/ServerStreamingMethodTests.cs index a6d0ce687..30b554e6b 100644 --- a/test/FunctionalTests/ServerStreamingMethodTests.cs +++ b/test/FunctionalTests/ServerStreamingMethodTests.cs @@ -26,6 +26,7 @@ using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; using Grpc.Core; +using Grpc.Tests; using NUnit.Framework; namespace Grpc.AspNetCore.FunctionalTests diff --git a/test/FunctionalTests/UnaryMethodTests.cs b/test/FunctionalTests/UnaryMethodTests.cs index 035078caa..9011b0f1b 100644 --- a/test/FunctionalTests/UnaryMethodTests.cs +++ b/test/FunctionalTests/UnaryMethodTests.cs @@ -28,8 +28,8 @@ using Greet; using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; -using Grpc.AspNetCore.Server.Tests; using Grpc.Core; +using Grpc.Tests; using NUnit.Framework; namespace Grpc.AspNetCore.FunctionalTests diff --git a/test/FunctionalTests/UnimplementedTests.cs b/test/FunctionalTests/UnimplementedTests.cs index d22594344..85a626b10 100644 --- a/test/FunctionalTests/UnimplementedTests.cs +++ b/test/FunctionalTests/UnimplementedTests.cs @@ -25,6 +25,7 @@ using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; using Grpc.Core; +using Grpc.Tests; using NUnit.Framework; namespace Grpc.AspNetCore.FunctionalTests diff --git a/test/Grpc.AspNetCore.Server.Tests/Grpc.AspNetCore.Server.Tests.csproj b/test/Grpc.AspNetCore.Server.Tests/Grpc.AspNetCore.Server.Tests.csproj index 7a262e836..898b201b3 100644 --- a/test/Grpc.AspNetCore.Server.Tests/Grpc.AspNetCore.Server.Tests.csproj +++ b/test/Grpc.AspNetCore.Server.Tests/Grpc.AspNetCore.Server.Tests.csproj @@ -8,17 +8,13 @@ - + - - - - diff --git a/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamReaderTests.cs b/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamReaderTests.cs index a58613e67..a5df11d86 100644 --- a/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamReaderTests.cs +++ b/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamReaderTests.cs @@ -23,6 +23,7 @@ using Greet; using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; +using Grpc.Tests; using Microsoft.AspNetCore.Http; using NUnit.Framework; diff --git a/test/Grpc.NetCore.HttpClient.Tests/AsyncClientStreamingCallTests.cs b/test/Grpc.NetCore.HttpClient.Tests/AsyncClientStreamingCallTests.cs new file mode 100644 index 000000000..1638222f7 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/AsyncClientStreamingCallTests.cs @@ -0,0 +1,204 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Greet; +using Grpc.Core; +using Grpc.NetCore.HttpClient.Internal; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using Grpc.Tests; +using NUnit.Framework; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class AsyncClientStreamingCallTests + { + [Test] + public async Task AsyncClientStreamingCall_Success_HttpRequestMessagePopulated() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + httpRequestMessage = request; + + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout(); + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + + await call.RequestStream.CompleteAsync().DefaultTimeout(); + + var response = await call; + + // Assert + Assert.AreEqual("Hello world", response.Message); + + Assert.IsNotNull(httpRequestMessage); + Assert.AreEqual(new Version(2, 0), httpRequestMessage.Version); + Assert.AreEqual(HttpMethod.Post, httpRequestMessage.Method); + Assert.AreEqual(new Uri("https://localhost/ServiceName/MethodName"), httpRequestMessage.RequestUri); + Assert.AreEqual(new MediaTypeHeaderValue("application/grpc"), httpRequestMessage.Content.Headers.ContentType); + } + + [Test] + public async Task AsyncClientStreamingCall_Success_RequestContentSent() + { + // Arrange + PushStreamContent content = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + content = (PushStreamContent)request.Content; + await content.PushComplete.DefaultTimeout(); + + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + + // Assert + Assert.IsNotNull(call); + Assert.IsNotNull(content); + + var responseTask = call.ResponseAsync; + Assert.IsFalse(responseTask.IsCompleted, "Response not returned until client stream is complete."); + + var streamTask = content.ReadAsStreamAsync(); + + await call.RequestStream.WriteAsync(new HelloRequest { Name = "1" }).DefaultTimeout(); + await call.RequestStream.WriteAsync(new HelloRequest { Name = "2" }).DefaultTimeout(); + + await call.RequestStream.CompleteAsync().DefaultTimeout(); + + var requestContent = await streamTask.DefaultTimeout(); + var requestMessage = await requestContent.ReadStreamedMessageAsync(TestHelpers.ServiceMethod.RequestMarshaller.Deserializer, CancellationToken.None).DefaultTimeout(); + Assert.AreEqual("1", requestMessage.Name); + requestMessage = await requestContent.ReadStreamedMessageAsync(TestHelpers.ServiceMethod.RequestMarshaller.Deserializer, CancellationToken.None).DefaultTimeout(); + Assert.AreEqual("2", requestMessage.Name); + + var responseMessage = await responseTask.DefaultTimeout(); + Assert.AreEqual("Hello world", responseMessage.Message); + } + + [Test] + public void ClientStreamWriter_WriteWhilePendingWrite_ErrorThrown() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply + { + Message = "Hello world" + }); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + + // Assert + var writeTask1 = call.RequestStream.WriteAsync(new HelloRequest { Name = "1" }); + Assert.IsFalse(writeTask1.IsCompleted); + + var writeTask2 = call.RequestStream.WriteAsync(new HelloRequest { Name = "2" }); + var ex = Assert.ThrowsAsync(() => writeTask2); + + Assert.AreEqual("Cannot write message because the previous write is in progress.", ex.Message); + } + + [Test] + public void ClientStreamWriter_CompleteWhilePendingWrite_ErrorThrown() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply + { + Message = "Hello world" + }); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + + // Assert + var writeTask1 = call.RequestStream.WriteAsync(new HelloRequest { Name = "1" }); + Assert.IsFalse(writeTask1.IsCompleted); + + var completeTask = call.RequestStream.CompleteAsync(); + var ex = Assert.ThrowsAsync(() => completeTask); + + Assert.AreEqual("Cannot complete client stream writer because the previous write is in progress.", ex.Message); + } + + [Test] + public async Task ClientStreamWriter_WriteWhileComplete_ErrorThrown() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply + { + Message = "Hello world" + }); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + await call.RequestStream.CompleteAsync(); + + // Assert + var ex = Assert.ThrowsAsync(() => call.RequestStream.WriteAsync(new HelloRequest { Name = "1" })); + + Assert.AreEqual("Cannot write message because the client stream writer is complete.", ex.Message); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/AsyncDuplexStreamingCallTests.cs b/test/Grpc.NetCore.HttpClient.Tests/AsyncDuplexStreamingCallTests.cs new file mode 100644 index 000000000..919735882 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/AsyncDuplexStreamingCallTests.cs @@ -0,0 +1,166 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Greet; +using Grpc.Core; +using Grpc.NetCore.HttpClient.Internal; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using Grpc.Tests; +using NUnit.Framework; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class AsyncDuplexStreamingCallTests + { + [Test] + public async Task AsyncDuplexStreamingCall_NoContent_NoMessagesReturned() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(request => + { + httpRequestMessage = request; + + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, new ByteArrayContent(Array.Empty()))); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncDuplexStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + + var responseStream = call.ResponseStream; + + // Assert + Assert.IsNull(responseStream.Current); + Assert.IsFalse(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout()); + Assert.IsNull(responseStream.Current); + } + + [Test] + public async Task AsyncServerStreamingCall_MessagesReturnedTogether_MessagesReceived() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(request => + { + httpRequestMessage = request; + + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, new ByteArrayContent(Array.Empty()))); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncDuplexStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + + var responseStream = call.ResponseStream; + + // Assert + Assert.IsNull(responseStream.Current); + Assert.IsFalse(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout()); + Assert.IsNull(responseStream.Current); + } + + [Test] + public async Task AsyncDuplexStreamingCall_MessagesStreamed_MessagesReceived() + { + // Arrange + var streamContent = new SyncPointMemoryStream(); + + PushStreamContent content = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + content = (PushStreamContent)request.Content; + await content.PushComplete.DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, new StreamContent(streamContent)); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncDuplexStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + + var requestStream = call.RequestStream; + var responseStream = call.ResponseStream; + + // Assert + await call.RequestStream.WriteAsync(new HelloRequest { Name = "1" }).DefaultTimeout(); + await call.RequestStream.WriteAsync(new HelloRequest { Name = "2" }).DefaultTimeout(); + + await call.RequestStream.CompleteAsync().DefaultTimeout(); + + var requestContent = await content.ReadAsStreamAsync().DefaultTimeout(); + var requestMessage = await requestContent.ReadStreamedMessageAsync(TestHelpers.ServiceMethod.RequestMarshaller.Deserializer, CancellationToken.None).DefaultTimeout(); + Assert.AreEqual("1", requestMessage.Name); + requestMessage = await requestContent.ReadStreamedMessageAsync(TestHelpers.ServiceMethod.RequestMarshaller.Deserializer, CancellationToken.None).DefaultTimeout(); + Assert.AreEqual("2", requestMessage.Name); + + Assert.IsNull(responseStream.Current); + + var moveNextTask1 = responseStream.MoveNext(CancellationToken.None); + Assert.IsFalse(moveNextTask1.IsCompleted); + + await streamContent.AddDataAndWait(await TestHelpers.GetResponseDataAsync(new HelloReply + { + Message = "Hello world 1" + }).DefaultTimeout()).DefaultTimeout(); + + Assert.IsTrue(await moveNextTask1.DefaultTimeout()); + Assert.IsNotNull(responseStream.Current); + Assert.AreEqual("Hello world 1", responseStream.Current.Message); + + var moveNextTask2 = responseStream.MoveNext(CancellationToken.None); + Assert.IsFalse(moveNextTask2.IsCompleted); + + await streamContent.AddDataAndWait(await TestHelpers.GetResponseDataAsync(new HelloReply + { + Message = "Hello world 2" + }).DefaultTimeout()).DefaultTimeout(); + + Assert.IsTrue(await moveNextTask2.DefaultTimeout()); + Assert.IsNotNull(responseStream.Current); + Assert.AreEqual("Hello world 2", responseStream.Current.Message); + + var moveNextTask3 = responseStream.MoveNext(CancellationToken.None); + Assert.IsFalse(moveNextTask3.IsCompleted); + + await streamContent.AddDataAndWait(Array.Empty()).DefaultTimeout(); + + Assert.IsFalse(await moveNextTask3.DefaultTimeout()); + Assert.IsFalse(await moveNextTask3.DefaultTimeout()); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/AsyncServerStreamingCallTests.cs b/test/Grpc.NetCore.HttpClient.Tests/AsyncServerStreamingCallTests.cs new file mode 100644 index 000000000..b041d238f --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/AsyncServerStreamingCallTests.cs @@ -0,0 +1,157 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Greet; +using Grpc.Core; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using Grpc.Tests; +using NUnit.Framework; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class AsyncServerStreamingCallTests + { + [Test] + public async Task AsyncServerStreamingCall_NoContent_NoMessagesReturned() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(request => + { + httpRequestMessage = request; + + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, new ByteArrayContent(Array.Empty()))); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + + var responseStream = call.ResponseStream; + + // Assert + Assert.IsNull(responseStream.Current); + Assert.IsFalse(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout()); + Assert.IsNull(responseStream.Current); + } + + [Test] + public async Task AsyncServerStreamingCall_MessagesReturnedTogether_MessagesReceived() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent( + new HelloReply + { + Message = "Hello world 1" + }, + new HelloReply + { + Message = "Hello world 2" + }).DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + + var responseStream = call.ResponseStream; + + // Assert + Assert.IsNull(responseStream.Current); + + Assert.IsTrue(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout()); + Assert.IsNotNull(responseStream.Current); + Assert.AreEqual("Hello world 1", responseStream.Current.Message); + + Assert.IsTrue(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout()); + Assert.IsNotNull(responseStream.Current); + Assert.AreEqual("Hello world 2", responseStream.Current.Message); + + Assert.IsFalse(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout()); + } + + [Test] + public async Task AsyncServerStreamingCall_MessagesStreamed_MessagesReceived() + { + // Arrange + var streamContent = new SyncPointMemoryStream(); + + var httpClient = TestHelpers.CreateTestClient(request => + { + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, new StreamContent(streamContent))); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + + var responseStream = call.ResponseStream; + + // Assert + Assert.IsNull(responseStream.Current); + + var moveNextTask1 = responseStream.MoveNext(CancellationToken.None); + Assert.IsFalse(moveNextTask1.IsCompleted); + + await streamContent.AddDataAndWait(await TestHelpers.GetResponseDataAsync(new HelloReply + { + Message = "Hello world 1" + }).DefaultTimeout()).DefaultTimeout(); + + Assert.IsTrue(await moveNextTask1.DefaultTimeout()); + Assert.IsNotNull(responseStream.Current); + Assert.AreEqual("Hello world 1", responseStream.Current.Message); + + var moveNextTask2 = responseStream.MoveNext(CancellationToken.None); + Assert.IsFalse(moveNextTask2.IsCompleted); + + await streamContent.AddDataAndWait(await TestHelpers.GetResponseDataAsync(new HelloReply + { + Message = "Hello world 2" + }).DefaultTimeout()).DefaultTimeout(); + + Assert.IsTrue(await moveNextTask2.DefaultTimeout()); + Assert.IsNotNull(responseStream.Current); + Assert.AreEqual("Hello world 2", responseStream.Current.Message); + + var moveNextTask3 = responseStream.MoveNext(CancellationToken.None); + Assert.IsFalse(moveNextTask3.IsCompleted); + + await streamContent.AddDataAndWait(Array.Empty()).DefaultTimeout(); + + Assert.IsFalse(await moveNextTask3.DefaultTimeout()); + Assert.IsFalse(await moveNextTask3.DefaultTimeout()); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/AsyncUnaryCallTests.cs b/test/Grpc.NetCore.HttpClient.Tests/AsyncUnaryCallTests.cs new file mode 100644 index 000000000..ca919e2db --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/AsyncUnaryCallTests.cs @@ -0,0 +1,105 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Greet; +using Grpc.Core; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using Grpc.Tests; +using NUnit.Framework; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class AsyncUnaryCallTests + { + [Test] + public async Task AsyncUnaryCall_Success_HttpRequestMessagePopulated() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + httpRequestMessage = request; + + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var rs = await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + + // Assert + Assert.AreEqual("Hello world", rs.Message); + + Assert.IsNotNull(httpRequestMessage); + Assert.AreEqual(new Version(2, 0), httpRequestMessage.Version); + Assert.AreEqual(HttpMethod.Post, httpRequestMessage.Method); + Assert.AreEqual(new Uri("https://localhost/ServiceName/MethodName"), httpRequestMessage.RequestUri); + Assert.AreEqual(new MediaTypeHeaderValue("application/grpc"), httpRequestMessage.Content.Headers.ContentType); + } + + [Test] + public async Task AsyncUnaryCall_Success_RequestContentSent() + { + // Arrange + HttpContent content = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + content = request.Content; + + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var rs = await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest { Name = "World" }); + + // Assert + Assert.AreEqual("Hello world", rs.Message); + + Assert.IsNotNull(content); + + var requestContent = await content.ReadAsStreamAsync().DefaultTimeout(); + var requestMessage = await requestContent.ReadSingleMessageAsync(TestHelpers.ServiceMethod.RequestMarshaller.Deserializer, CancellationToken.None).DefaultTimeout(); + + Assert.AreEqual("World", requestMessage.Name); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs b/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs new file mode 100644 index 000000000..48b6c0c60 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs @@ -0,0 +1,133 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Greet; +using Grpc.Core; +using Grpc.NetCore.HttpClient.Internal; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using Grpc.Tests; +using NUnit.Framework; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class DeadlineTests + { + [Test] + public async Task AsyncUnaryCall_SetSecondDeadline_RequestMessageContainsDeadlineHeader() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + httpRequestMessage = request; + + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + invoker.Clock = new TestSystemClock(new DateTime(2019, 11, 29, 1, 1, 1, DateTimeKind.Utc)); + + // Act + await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: invoker.Clock.UtcNow.AddSeconds(1)), new HelloRequest()); + + // Assert + Assert.IsNotNull(httpRequestMessage); + Assert.AreEqual(1, httpRequestMessage.Headers.Count()); + Assert.AreEqual("1000m", httpRequestMessage.Headers.GetValues("grpc-timeout").Single()); + } + + [Test] + public async Task AsyncUnaryCall_SetMaxValueDeadline_RequestMessageHasNoDeadlineHeader() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + httpRequestMessage = request; + + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: DateTime.MaxValue), new HelloRequest()); + + // Assert + Assert.IsNotNull(httpRequestMessage); + Assert.AreEqual(0, httpRequestMessage.Headers.Count()); + } + + [Test] + public async Task AsyncUnaryCall_SendDeadlineHeaderAndDeadlineValue_DeadlineValueIsUsed() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + httpRequestMessage = request; + + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + invoker.Clock = new TestSystemClock(new DateTime(2019, 11, 29, 1, 1, 1, DateTimeKind.Utc)); + + var headers = new Metadata(); + headers.Add("grpc-timeout", "1D"); + + // Act + var rs = await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(headers: headers, deadline: invoker.Clock.UtcNow.AddSeconds(1)), new HelloRequest()); + + // Assert + Assert.AreEqual("Hello world", rs.Message); + + Assert.IsNotNull(httpRequestMessage); + Assert.AreEqual(1, httpRequestMessage.Headers.Count()); + Assert.AreEqual("1000m", httpRequestMessage.Headers.GetValues("grpc-timeout").Single()); + } + + private class TestSystemClock : ISystemClock + { + public TestSystemClock(DateTime utcNow) + { + UtcNow = utcNow; + } + + public DateTime UtcNow { get; } + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs b/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs new file mode 100644 index 000000000..175f4fac2 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs @@ -0,0 +1,305 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using NUnit.Framework; +using Greet; +using static Greet.Greeter; +using Grpc.Core; +using Google.Protobuf; +using System.Net.Http; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using System.Net; +using System.Threading.Tasks; +using System; +using System.IO; +using System.Threading; +using System.Net.Http.Headers; +using System.Text; +using System.Linq; +using Grpc.NetCore.HttpClient.Internal; +using Grpc.Tests; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class GetTrailersTests + { + [Test] + public async Task AsyncUnaryCall_MessageReturned_ReturnsTrailers() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + response.Headers.Add("custom", "ABC"); + response.TrailingHeaders.Add("custom-header", "value"); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + var message = await call; + var trailers1 = call.GetTrailers(); + var trailers2 = call.GetTrailers(); + + // Assert + Assert.AreSame(trailers1, trailers2); + Assert.AreEqual(1, trailers1.Count); + Assert.AreEqual("value", trailers1.Single(t => t.Key == "custom-header").Value); + } + + [Test] + public async Task AsyncUnaryCall_HeadersReturned_ReturnsTrailers() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + response.Headers.Add("custom", "ABC"); + response.TrailingHeaders.Add("custom-header", "value"); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + var responseHeaders = await call.ResponseHeadersAsync.DefaultTimeout(); + var trailers = call.GetTrailers(); + + // Assert + Assert.AreEqual(1, trailers.Count); + Assert.AreEqual("value", trailers.Single(t => t.Key == "custom-header").Value); + } + + [Test] + public void AsyncUnaryCall_UnfinishedCall_ThrowsError() + { + // Arrange + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var httpClient = TestHelpers.CreateTestClient(async request => + { + await tcs.Task.DefaultTimeout(); + return null; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + var ex = Assert.Throws(() => call.GetTrailers()); + + // Assert + Assert.AreEqual("Can't get the call trailers because the call is not complete.", ex.Message); + } + + [Test] + public void AsyncUnaryCall_ErrorCall_ThrowsError() + { + // Arrange + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var httpClient = TestHelpers.CreateTestClient(request => + { + return Task.FromException(new Exception("An error!")); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + var ex = Assert.Throws(() => call.GetTrailers()); + + // Assert + Assert.AreEqual("Can't get the call trailers because an error occured when making the request.", ex.Message); + Assert.AreEqual("An error!", ex.InnerException.InnerException.Message); + } + + [Test] + public void AsyncClientStreamingCall_UnfinishedCall_ThrowsError() + { + // Arrange + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var httpClient = TestHelpers.CreateTestClient(async request => + { + await tcs.Task.DefaultTimeout(); + return null; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + var ex = Assert.Throws(() => call.GetTrailers()); + + // Assert + Assert.AreEqual("Can't get the call trailers because the call is not complete.", ex.Message); + } + + [Test] + public void AsyncServerStreamingCall_UnfinishedCall_ThrowsError() + { + // Arrange + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var httpClient = TestHelpers.CreateTestClient(async request => + { + await tcs.Task.DefaultTimeout(); + return null; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + var ex = Assert.Throws(() => call.GetTrailers()); + + // Assert + Assert.AreEqual("Can't get the call trailers because the call is not complete.", ex.Message); + } + + [Test] + public async Task AsyncServerStreamingCall_UnfinishedReader_ThrowsError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent( + new HelloReply + { + Message = "Hello world 1" + }, + new HelloReply + { + Message = "Hello world 2" + }).DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + var responseStream = call.ResponseStream; + + Assert.IsTrue(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout()); + var ex = Assert.Throws(() => call.GetTrailers()); + + // Assert + Assert.AreEqual("Can't get the call trailers because the call is not complete.", ex.Message); + } + + [Test] + public async Task AsyncServerStreamingCall_FinishedReader_ReturnsTrailers() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent( + new HelloReply + { + Message = "Hello world 1" + }, + new HelloReply + { + Message = "Hello world 2" + }).DefaultTimeout(); + + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + response.TrailingHeaders.Add("custom-header", "value"); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + var responseStream = call.ResponseStream; + + Assert.IsTrue(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout()); + Assert.IsTrue(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout()); + Assert.IsFalse(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout()); + var trailers = call.GetTrailers(); + + // Assert + Assert.AreEqual(1, trailers.Count); + Assert.AreEqual("value", trailers.Single(t => t.Key == "custom-header").Value); + } + + [Test] + public async Task AsyncClientStreamingCall_CompleteWriter_ReturnsTrailers() + { + // Arrange + var trailingHeadersWrittenTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var httpClient = TestHelpers.CreateTestClient(request => + { + var content = (PushStreamContent)request.Content; + var stream = new SyncPointMemoryStream(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, new StreamContent(stream)); + + _ = Task.Run(async () => + { + // Add a response message after the client has completed + await content.PushComplete.DefaultTimeout(); + + var messageData = await TestHelpers.GetResponseDataAsync(new HelloReply { Message = "Hello world" }).DefaultTimeout(); + await stream.AddDataAndWait(messageData).DefaultTimeout(); + await stream.AddDataAndWait(Array.Empty()).DefaultTimeout(); + + response.TrailingHeaders.Add("custom-header", "value"); + trailingHeadersWrittenTcs.SetResult(true); + }); + + return Task.FromResult(response); + }); + + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + await call.RequestStream.CompleteAsync().DefaultTimeout(); + await Task.WhenAll(call.ResponseAsync.DefaultTimeout(), trailingHeadersWrittenTcs.Task); + var trailers = call.GetTrailers(); + + // Assert + Assert.AreEqual(1, trailers.Count); + Assert.AreEqual("value", trailers.Single(t => t.Key == "custom-header").Value); + } + + [Test] + public void AsyncClientStreamingCall_UncompleteWriter_ThrowsError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var stream = new SyncPointMemoryStream(); + + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, new StreamContent(stream)); + return Task.FromResult(response); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + var ex = Assert.Throws(() => call.GetTrailers()); + + // Assert + Assert.AreEqual("Can't get the call trailers because the call is not complete.", ex.Message); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/Grpc.NetCore.HttpClient.Tests.csproj b/test/Grpc.NetCore.HttpClient.Tests/Grpc.NetCore.HttpClient.Tests.csproj new file mode 100644 index 000000000..0c7ddd570 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/Grpc.NetCore.HttpClient.Tests.csproj @@ -0,0 +1,30 @@ + + + + netcoreapp3.0 + false + latest + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Grpc.NetCore.HttpClient.Tests/GrpcClientFactoryTests.cs b/test/Grpc.NetCore.HttpClient.Tests/GrpcClientFactoryTests.cs new file mode 100644 index 000000000..1e5a4b518 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/GrpcClientFactoryTests.cs @@ -0,0 +1,38 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using NUnit.Framework; +using Greet; +using static Greet.Greeter; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class GrpcClientFactoryTests + { + [Test] + public void Create_WithBaseAddress_ReturnInstance() + { + // Arrange & Act + var client = GrpcClientFactory.Create("http://localhost"); + + // Assert + Assert.IsNotNull(client); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/HeadersTests.cs b/test/Grpc.NetCore.HttpClient.Tests/HeadersTests.cs new file mode 100644 index 000000000..803587e26 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/HeadersTests.cs @@ -0,0 +1,108 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Greet; +using Grpc.Core; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using Grpc.Tests; +using NUnit.Framework; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class HeadersTests + { + [Test] + public async Task AsyncUnaryCall_SendHeaders_RequestMessageContainsHeaders() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + httpRequestMessage = request; + + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + var headers = new Metadata(); + headers.Add("custom", "ascii"); + headers.Add("custom-bin", Encoding.UTF8.GetBytes("Hello world")); + + // Act + var rs = await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(headers: headers), new HelloRequest()); + + // Assert + Assert.AreEqual("Hello world", rs.Message); + + Assert.IsNotNull(httpRequestMessage); + Assert.AreEqual(2, httpRequestMessage.Headers.Count()); + Assert.AreEqual("ascii", httpRequestMessage.Headers.GetValues("custom").Single()); + Assert.AreEqual("Hello world", Encoding.UTF8.GetString(Convert.FromBase64String(httpRequestMessage.Headers.GetValues("custom-bin").Single()))); + } + + [Test] + public async Task AsyncUnaryCall_NoHeaders_RequestMessageHasNoHeaders() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + httpRequestMessage = request; + + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + var headers = new Metadata(); + + // Act + var rs = await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(headers: headers), new HelloRequest()); + + // Assert + Assert.AreEqual("Hello world", rs.Message); + + Assert.IsNotNull(httpRequestMessage); + Assert.AreEqual(0, httpRequestMessage.Headers.Count()); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/ResponseUtils.cs b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/ResponseUtils.cs new file mode 100644 index 000000000..972252ef3 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/ResponseUtils.cs @@ -0,0 +1,72 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System.Net.Http; +using System; +using System.Net; +using System.IO; +using System.Threading.Tasks; +using System.Threading; +using System.Diagnostics; +using System.Buffers.Binary; + +namespace Grpc.NetCore.HttpClient.Tests.Infrastructure +{ + internal static class ResponseUtils + { + public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode) => + CreateResponse(statusCode, string.Empty); + + public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, string payload) => + CreateResponse(statusCode, new StringContent(payload)); + + public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, byte[] payload) => + CreateResponse(statusCode, new ByteArrayContent(payload)); + + public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, HttpContent payload) + { + return new HttpResponseMessage(statusCode) + { + Content = payload + }; + } + + private const int MessageDelimiterSize = 4; // how many bytes it takes to encode "Message-Length" + private const int HeaderSize = MessageDelimiterSize + 1; // message length + compression flag + + public static Task WriteHeaderAsync(Stream stream, int length, bool compress, CancellationToken cancellationToken) + { + var headerData = new byte[HeaderSize]; + + // Compression flag + headerData[0] = compress ? (byte)1 : (byte)0; + + // Message length + EncodeMessageLength(length, headerData.AsSpan(1)); + + return stream.WriteAsync(headerData, 0, headerData.Length, cancellationToken); + } + + private static void EncodeMessageLength(int messageLength, Span destination) + { + Debug.Assert(destination.Length >= MessageDelimiterSize, "Buffer too small to encode message length."); + + BinaryPrimitives.WriteUInt32BigEndian(destination, (uint)messageLength); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/TestHelpers.cs b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/TestHelpers.cs new file mode 100644 index 000000000..261622d2d --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/TestHelpers.cs @@ -0,0 +1,82 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Google.Protobuf; +using Greet; +using Grpc.Core; + +namespace Grpc.NetCore.HttpClient.Tests.Infrastructure +{ + public static class TestHelpers + { + public static readonly Marshaller HelloRequestMarshaller = Marshallers.Create(r => r.ToByteArray(), data => HelloRequest.Parser.ParseFrom(data)); + public static readonly Marshaller HelloReplyMarshaller = Marshallers.Create(r => r.ToByteArray(), data => HelloReply.Parser.ParseFrom(data)); + + public static readonly Method ServiceMethod = new Method(MethodType.Unary, "ServiceName", "MethodName", HelloRequestMarshaller, HelloReplyMarshaller); + + public static System.Net.Http.HttpClient CreateTestClient(Func> sendAsync) + { + var handler = TestHttpMessageHandler.Create(sendAsync); + var httpClient = new System.Net.Http.HttpClient(handler); + httpClient.BaseAddress = new Uri("https://localhost"); + + return httpClient; + } + + public static System.Net.Http.HttpClient CreateTestClient(Func> sendAsync) + { + var handler = TestHttpMessageHandler.Create(sendAsync); + var httpClient = new System.Net.Http.HttpClient(handler); + httpClient.BaseAddress = new Uri("https://localhost"); + + return httpClient; + } + + public static async Task CreateResponseContent(params TResponse[] responses) where TResponse : IMessage + { + var ms = new MemoryStream(); + foreach (var response in responses) + { + await WriteResponseAsync(ms, response); + } + ms.Seek(0, SeekOrigin.Begin); + var streamContent = new StreamContent(ms); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/grpc"); + return streamContent; + } + + public static async Task WriteResponseAsync(Stream ms, TResponse response) where TResponse : IMessage + { + await ResponseUtils.WriteHeaderAsync(ms, response.CalculateSize(), false, CancellationToken.None); + await ms.WriteAsync(response.ToByteArray()); + } + + public static async Task GetResponseDataAsync(TResponse response) where TResponse : IMessage + { + var ms = new MemoryStream(); + await WriteResponseAsync(ms, response); + return ms.ToArray(); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/TestHttpMessageHandler.cs b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/TestHttpMessageHandler.cs new file mode 100644 index 000000000..895cb42e4 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/TestHttpMessageHandler.cs @@ -0,0 +1,50 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System; + +namespace Grpc.NetCore.HttpClient.Tests.Infrastructure +{ + public class TestHttpMessageHandler : HttpMessageHandler + { + private readonly Func> _sendAsync; + + public TestHttpMessageHandler(Func> sendAsync) + { + _sendAsync = sendAsync; + } + + public static TestHttpMessageHandler Create(Func> sendAsync) + { + return new TestHttpMessageHandler((request, cancellationToken) => sendAsync(request)); + } + + public static TestHttpMessageHandler Create(Func> sendAsync) + { + return new TestHttpMessageHandler(sendAsync); + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return _sendAsync(request, cancellationToken); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/Proto/greet.proto b/test/Grpc.NetCore.HttpClient.Tests/Proto/greet.proto new file mode 100644 index 000000000..85fb59318 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/Proto/greet.proto @@ -0,0 +1,35 @@ +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package Greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply) {} + rpc SayHellos (HelloRequest) returns (stream HelloReply) {} +} + +service SecondGreeter { + rpc SayHello (HelloRequest) returns (HelloReply) {} + rpc SayHellos (HelloRequest) returns (stream HelloReply) {} +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/ResponseAsyncTests.cs b/test/Grpc.NetCore.HttpClient.Tests/ResponseAsyncTests.cs new file mode 100644 index 000000000..94213ccaa --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/ResponseAsyncTests.cs @@ -0,0 +1,112 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Greet; +using Grpc.Core; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using Grpc.Tests; +using NUnit.Framework; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class ResponseAsyncTests + { + [Test] + public async Task AsyncUnaryCall_AwaitMultipleTimes_SameMessageReturned() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest { Name = "World" }); + + var response1 = await call; + var response2 = await call; + var response3 = await call.ResponseAsync.DefaultTimeout(); + var response4 = await call.ResponseAsync.DefaultTimeout(); + + // Assert + Assert.AreEqual("Hello world", response1.Message); + + Assert.AreEqual(response1, response2); + Assert.AreEqual(response1, response3); + Assert.AreEqual(response1, response4); + } + + [Test] + public async Task AsyncUnaryCall_DisposeAfterHeadersAndBeforeMessage_ThrowsError() + { + // Arrange + var stream = new SyncPointMemoryStream(); + + var httpClient = TestHelpers.CreateTestClient(request => + { + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, new StreamContent(stream)); + response.Headers.Add("custom", "value!"); + return Task.FromResult(response); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest { Name = "World" }); + var responseHeaders = await call.ResponseHeadersAsync.DefaultTimeout(); + call.Dispose(); + + // Assert + Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout()); + + var header = responseHeaders.Single(h => h.Key == "custom"); + Assert.AreEqual("value!", header.Value); + } + + [Test] + public void AsyncUnaryCall_ErrorSendingRequest_ThrowsError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + return Task.FromException(new Exception("An error!")); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + var ex = Assert.CatchAsync(() => call.ResponseAsync); + + // Assert + Assert.AreEqual("An error!", ex.Message); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/ResponseHeadersAsyncTests.cs b/test/Grpc.NetCore.HttpClient.Tests/ResponseHeadersAsyncTests.cs new file mode 100644 index 000000000..8b631226c --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/ResponseHeadersAsyncTests.cs @@ -0,0 +1,186 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Greet; +using Grpc.Core; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using Grpc.Tests; +using NUnit.Framework; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class ResponseHeadersAsyncTests + { + [Test] + public async Task AsyncUnaryCall_Success_ResponseHeadersPopulated() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + response.Headers.Server.Add(new ProductInfoHeaderValue("TestName", "1.0")); + response.Headers.Add("custom", "ABC"); + response.Headers.Add("binary-bin", Convert.ToBase64String(Encoding.UTF8.GetBytes("Hello world"))); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + var responseHeaders1 = await call.ResponseHeadersAsync.DefaultTimeout(); + var responseHeaders2 = await call.ResponseHeadersAsync.DefaultTimeout(); + + // Assert + Assert.AreSame(responseHeaders1, responseHeaders2); + + var header = responseHeaders1.Single(h => h.Key == "server"); + Assert.AreEqual("TestName/1.0", header.Value); + + header = responseHeaders1.Single(h => h.Key == "custom"); + Assert.AreEqual("ABC", header.Value); + + header = responseHeaders1.Single(h => h.Key == "binary-bin"); + Assert.AreEqual(true, header.IsBinary); + CollectionAssert.AreEqual(Encoding.UTF8.GetBytes("Hello world"), header.ValueBytes); + } + + [Test] + public async Task AsyncClientStreamingCall_Success_ResponseHeadersPopulated() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + response.Headers.Add("custom", "ABC"); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + var responseHeaders = await call.ResponseHeadersAsync.DefaultTimeout(); + + // Assert + var header = responseHeaders.Single(h => h.Key == "custom"); + Assert.AreEqual("ABC", header.Value); + } + + [Test] + public async Task AsyncDuplexStreamingCall_Success_ResponseHeadersPopulated() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + response.Headers.Add("custom", "ABC"); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncDuplexStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + var responseHeaders = await call.ResponseHeadersAsync.DefaultTimeout(); + + // Assert + var header = responseHeaders.Single(h => h.Key == "custom"); + Assert.AreEqual("ABC", header.Value); + } + + [Test] + public async Task AsyncServerStreamingCall_Success_ResponseHeadersPopulated() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + response.Headers.Add("custom", "ABC"); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + var responseHeaders = await call.ResponseHeadersAsync.DefaultTimeout(); + + // Assert + var header = responseHeaders.Single(h => h.Key == "custom"); + Assert.AreEqual("ABC", header.Value); + } + + [Test] + public void AsyncServerStreamingCall_ErrorSendingRequest_ReturnsError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + return Task.FromException(new Exception("An error!")); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + var ex = Assert.CatchAsync(() => call.ResponseHeadersAsync); + + // Assert + Assert.AreEqual("An error!", ex.Message); + } + + [Test] + public void AsyncServerStreamingCall_DisposeBeforeHeadersReceived_ReturnsError() + { + // Arrange + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var httpClient = TestHelpers.CreateTestClient(async (request, ct) => + { + await tcs.Task.DefaultTimeout(); + ct.ThrowIfCancellationRequested(); + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + response.Headers.Add("custom", "ABC"); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + call.Dispose(); + tcs.TrySetResult(true); + + // Assert + Assert.CatchAsync(() => call.ResponseHeadersAsync); + } + } +} diff --git a/test/Shared/SyncPoint.cs b/test/Shared/SyncPoint.cs index 1d722a50e..2ab83eee8 100644 --- a/test/Shared/SyncPoint.cs +++ b/test/Shared/SyncPoint.cs @@ -20,7 +20,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Grpc.AspNetCore.Server.Tests +namespace Grpc.Tests { public class SyncPoint { diff --git a/test/Shared/SyncPointMemoryStream.cs b/test/Shared/SyncPointMemoryStream.cs index c9e368f3f..78b77f625 100644 --- a/test/Shared/SyncPointMemoryStream.cs +++ b/test/Shared/SyncPointMemoryStream.cs @@ -22,7 +22,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Grpc.AspNetCore.Server.Tests +namespace Grpc.Tests { /// /// A memory stream that waits for data when reading and allows the sender of data to wait for it to be read. diff --git a/test/FunctionalTests/Infrastructure/TaskExtensions.cs b/test/Shared/TaskExtensions.cs similarity index 98% rename from test/FunctionalTests/Infrastructure/TaskExtensions.cs rename to test/Shared/TaskExtensions.cs index 894c82a84..3267ffbf9 100644 --- a/test/FunctionalTests/Infrastructure/TaskExtensions.cs +++ b/test/Shared/TaskExtensions.cs @@ -22,7 +22,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Grpc.AspNetCore.FunctionalTests.Infrastructure +namespace Grpc.Tests { internal static class TaskExtensions { From 8398d7651b75a81191f51c44d82a315e6bbcdd02 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 25 Apr 2019 23:46:04 +1200 Subject: [PATCH 02/13] Interop test suite integration --- Grpc.AspNetCore.sln | 14 + build/sources.props | 1 + global.json | 2 +- .../BenchmarkServer/BenchmarkServer.csproj | 1 + .../Internal/GrpcProtocolHelpers.cs | 2 +- .../HttpClientCallInvoker.cs | 5 +- .../Internal/EmptyContent.cs | 39 + .../Internal/GrpcCall.cs | 63 +- .../Internal/GrpcProtocolConstants.cs | 7 + .../Internal/HttpContextClientStreamReader.cs | 10 +- .../AsyncUnaryCallTests.cs | 18 + .../GetTrailersTests.cs | 4 - .../Infrastructure/ResponseUtils.cs | 10 +- testassets/FunctionalTestsWebsite/Startup.cs | 1 - testassets/InteropTestsClient/Certs/README.md | 8 + testassets/InteropTestsClient/Certs/ca.pem | 15 + .../InteropTestsClient/Certs/server1.key | 16 + .../InteropTestsClient/Certs/server1.pem | 16 + .../InteropTestsClient/Certs/server1.pfx | Bin 0 -> 2325 bytes testassets/InteropTestsClient/IChannel.cs | 36 + .../InteropTestsClient/InteropClient.cs | 749 ++++++++++++++++++ .../InteropTestsClient.csproj | 24 + testassets/InteropTestsClient/Program.cs | 28 + testassets/InteropTestsClient/RunTests.ps1 | 35 + .../InteropTestsClient/TestCredentials.cs | 75 ++ testassets/InteropTestsWebsite/Program.cs | 3 + .../InteropTestsWebsite/TestServiceImpl.cs | 18 + testassets/Proto/empty.proto | 28 + testassets/Proto/messages.proto | 165 ++++ testassets/Proto/test.proto | 79 ++ 30 files changed, 1446 insertions(+), 26 deletions(-) create mode 100644 src/Grpc.NetCore.HttpClient/Internal/EmptyContent.cs create mode 100644 testassets/InteropTestsClient/Certs/README.md create mode 100644 testassets/InteropTestsClient/Certs/ca.pem create mode 100644 testassets/InteropTestsClient/Certs/server1.key create mode 100644 testassets/InteropTestsClient/Certs/server1.pem create mode 100644 testassets/InteropTestsClient/Certs/server1.pfx create mode 100644 testassets/InteropTestsClient/IChannel.cs create mode 100644 testassets/InteropTestsClient/InteropClient.cs create mode 100644 testassets/InteropTestsClient/InteropTestsClient.csproj create mode 100644 testassets/InteropTestsClient/Program.cs create mode 100644 testassets/InteropTestsClient/RunTests.ps1 create mode 100644 testassets/InteropTestsClient/TestCredentials.cs create mode 100644 testassets/Proto/empty.proto create mode 100644 testassets/Proto/messages.proto create mode 100644 testassets/Proto/test.proto diff --git a/Grpc.AspNetCore.sln b/Grpc.AspNetCore.sln index 0f684fd08..bbddb2873 100644 --- a/Grpc.AspNetCore.sln +++ b/Grpc.AspNetCore.sln @@ -56,11 +56,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testassets", "testassets", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Proto", "Proto", "{BF1393D4-6099-4EF9-85BB-7EE6CBEB920C}" ProjectSection(SolutionItems) = preProject + testassets\Proto\authorize.proto = testassets\Proto\authorize.proto testassets\Proto\chat.proto = testassets\Proto\chat.proto testassets\Proto\compression.proto = testassets\Proto\compression.proto testassets\Proto\count.proto = testassets\Proto\count.proto + testassets\Proto\empty.proto = testassets\Proto\empty.proto testassets\Proto\greet.proto = testassets\Proto\greet.proto + testassets\Proto\lifetime.proto = testassets\Proto\lifetime.proto testassets\Proto\message.proto = testassets\Proto\message.proto + testassets\Proto\messages.proto = testassets\Proto\messages.proto + testassets\Proto\nested.proto = testassets\Proto\nested.proto + testassets\Proto\singleton.proto = testassets\Proto\singleton.proto + testassets\Proto\test.proto = testassets\Proto\test.proto EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FunctionalTestsWebsite", "testassets\FunctionalTestsWebsite\FunctionalTestsWebsite.csproj", "{7B95289B-4992-4C0D-B26F-8EC58F81FC96}" @@ -110,6 +117,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Grpc.AspNetCore.Server.Refl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reflector", "examples\Clients\Reflector\Reflector.csproj", "{86AD33E9-2C07-45BD-B599-420C2618188D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropTestsClient", "testassets\InteropTestsClient\InteropTestsClient.csproj", "{291E5BA5-608D-406D-A2DB-389412D907F3}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.NetCore.HttpClient.Tests", "test\Grpc.NetCore.HttpClient.Tests\Grpc.NetCore.HttpClient.Tests.csproj", "{2D26FB7A-72AD-41B9-9B06-44F50A8F8A49}" EndProject Global @@ -194,6 +203,10 @@ Global {2D26FB7A-72AD-41B9-9B06-44F50A8F8A49}.Debug|Any CPU.Build.0 = Debug|Any CPU {2D26FB7A-72AD-41B9-9B06-44F50A8F8A49}.Release|Any CPU.ActiveCfg = Release|Any CPU {2D26FB7A-72AD-41B9-9B06-44F50A8F8A49}.Release|Any CPU.Build.0 = Release|Any CPU + {291E5BA5-608D-406D-A2DB-389412D907F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {291E5BA5-608D-406D-A2DB-389412D907F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {291E5BA5-608D-406D-A2DB-389412D907F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {291E5BA5-608D-406D-A2DB-389412D907F3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -228,6 +241,7 @@ Global {55813F20-1269-4B19-B03E-7E4A90148F92} = {8C62055F-8CD7-4859-9001-634D544DF2AE} {86AD33E9-2C07-45BD-B599-420C2618188D} = {F6E0F9D7-64E5-4C7B-A9BC-3C2AD687710B} {2D26FB7A-72AD-41B9-9B06-44F50A8F8A49} = {CECC4AE8-9C4E-4727-939B-517CC2E58D65} + {291E5BA5-608D-406D-A2DB-389412D907F3} = {59C7B1F0-EE4D-4098-8596-0ADDBC305234} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {CD5C2B19-49B4-480A-990C-36D98A719B07} diff --git a/build/sources.props b/build/sources.props index 104d54c7c..2d2fb609b 100644 --- a/build/sources.props +++ b/build/sources.props @@ -4,6 +4,7 @@ $(RestoreSources); https://api.nuget.org/v3/index.json; https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; + https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json; $(RestoreSources); diff --git a/global.json b/global.json index 04bcb8999..aa30320ef 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "3.0.100-preview4-011108" + "version": "3.0.100-preview6-011568" } } diff --git a/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj b/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj index b769e7b29..810045d5b 100644 --- a/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj +++ b/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj @@ -35,5 +35,6 @@ + diff --git a/src/Grpc.AspNetCore.Server/Internal/GrpcProtocolHelpers.cs b/src/Grpc.AspNetCore.Server/Internal/GrpcProtocolHelpers.cs index c62aa9ab0..162fd1bb2 100644 --- a/src/Grpc.AspNetCore.Server/Internal/GrpcProtocolHelpers.cs +++ b/src/Grpc.AspNetCore.Server/Internal/GrpcProtocolHelpers.cs @@ -205,7 +205,7 @@ public static void SetStatusTrailers(HttpResponse response, Status status) { // Use SetTrailer here because we want to overwrite any that was set earlier SetTrailer(response, GrpcProtocolConstants.StatusTrailer, status.StatusCode.ToTrailerString()); - SetTrailer(response, GrpcProtocolConstants.MessageTrailer, status.Detail); + SetTrailer(response, GrpcProtocolConstants.MessageTrailer, !string.IsNullOrEmpty(status.Detail) ? status.Detail : null); } private static void SetTrailer(HttpResponse response, string trailerName, StringValues trailerValues) diff --git a/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs b/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs index 94216ace6..2c6f7a765 100644 --- a/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs +++ b/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs @@ -111,7 +111,7 @@ public override AsyncDuplexStreamingCall AsyncDuplexStreami public override AsyncServerStreamingCall AsyncServerStreamingCall(Method method, string host, CallOptions options, TRequest request) { var call = CreateGrpcCall(method, options); - call.SendServerStreaming(_client); + call.SendServerStreaming(_client, request); return new AsyncServerStreamingCall( responseStream: call.StreamReader, @@ -142,7 +142,8 @@ public override AsyncUnaryCall AsyncUnaryCall(Me /// public override TResponse BlockingUnaryCall(Method method, string host, CallOptions options, TRequest request) { - return AsyncUnaryCall(method, host, options, request)?.GetAwaiter().GetResult(); + var call = AsyncUnaryCall(method, host, options, request); + return call.ResponseAsync.GetAwaiter().GetResult(); } private GrpcCall CreateGrpcCall(Method method, CallOptions options) diff --git a/src/Grpc.NetCore.HttpClient/Internal/EmptyContent.cs b/src/Grpc.NetCore.HttpClient/Internal/EmptyContent.cs new file mode 100644 index 000000000..d94ebf39b --- /dev/null +++ b/src/Grpc.NetCore.HttpClient/Internal/EmptyContent.cs @@ -0,0 +1,39 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Grpc.NetCore.HttpClient.Internal +{ + internal class EmptyContent : HttpContent + { + protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + return Task.CompletedTask; + } + + protected override bool TryComputeLength(out long length) + { + length = -1; + return false; + } + } +} diff --git a/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs index 2cfaef566..bedececf1 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs @@ -36,7 +36,7 @@ internal class GrpcCall private Metadata _trailers; public bool Disposed { get; private set; } - public bool ResponseFinished { get; set; } + public bool ResponseFinished { get; private set; } public CallOptions Options { get; } public Method Method { get; } public Task SendTask { get; private set; } @@ -60,15 +60,18 @@ public CancellationToken CancellationToken public void SendUnary(System.Net.Http.HttpClient client, TRequest request) { HttpRequestMessage message = CreateHttpRequestMessage(); + SetMessageContent(request, message); + SendCore(client, message); + } + private void SetMessageContent(TRequest request, HttpRequestMessage message) + { message.Content = new PushStreamContent( (stream) => { return SerialiationHelpers.WriteMessage(stream, request, Method.RequestMarshaller.Serializer, Options.CancellationToken); }, GrpcProtocolConstants.GrpcContentTypeHeaderValue); - - SendCore(client, message); } public void SendClientStreaming(System.Net.Http.HttpClient client) @@ -79,10 +82,10 @@ public void SendClientStreaming(System.Net.Http.HttpClient client) SendCore(client, message); } - public void SendServerStreaming(System.Net.Http.HttpClient client) + public void SendServerStreaming(System.Net.Http.HttpClient client, TRequest request) { HttpRequestMessage message = CreateHttpRequestMessage(); - + SetMessageContent(request, message); SendCore(client, message); StreamReader = new HttpContextClientStreamReader(this); @@ -176,15 +179,40 @@ public void EnsureNotDisposed() public async Task GetResponseAsync() { _httpResponse = await SendTask.ConfigureAwait(false); - var responseStream = await _httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); + // Server might have returned a status without any response body. For example, an unimplemented status + // Check for the trailer status before attempting to read the body and failing + if (_httpResponse.TrailingHeaders.Contains(GrpcProtocolConstants.StatusTrailer)) + { + FinishResponse(_httpResponse); + } + + var responseStream = await _httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); var message = await responseStream.ReadSingleMessageAsync(Method.ResponseMarshaller.Deserializer, _callCts.Token).ConfigureAwait(false); - ResponseFinished = true; + FinishResponse(_httpResponse); // The task of this method is cached so there is no need to cache the message here return message; } + internal void FinishResponse(HttpResponseMessage httpResponseMessage) + { + if (ResponseFinished) + { + return; + } + + ResponseFinished = true; + + _httpResponse = httpResponseMessage; + + var status = GetStatusCore(_httpResponse); + if (status.StatusCode != StatusCode.OK) + { + throw new RpcException(status); + } + } + public async Task GetResponseHeadersAsync() { _httpResponse = await SendTask.ConfigureAwait(false); @@ -197,19 +225,30 @@ public Status GetStatus() { ValidateTrailersAvailable(); - var grpcStatus = SendTask.Result.TrailingHeaders.GetValues(GrpcProtocolConstants.StatusTrailer).FirstOrDefault(); - var grpcMessage = SendTask.Result.TrailingHeaders.GetValues(GrpcProtocolConstants.MessageTrailer).FirstOrDefault(); + return GetStatusCore(_httpResponse); + } - int statusValue; - if (grpcStatus == null) + private static Status GetStatusCore(HttpResponseMessage httpResponseMessage) + { + string grpcStatus; + if (!httpResponseMessage.TrailingHeaders.TryGetValues(GrpcProtocolConstants.StatusTrailer, out var grpcStatusValues) || + (grpcStatus = grpcStatusValues.FirstOrDefault()) == null) { throw new InvalidOperationException("Response did not have a grpc-status trailer."); } - else if (!int.TryParse(grpcStatus, out statusValue)) + + int statusValue; + if (!int.TryParse(grpcStatus, out statusValue)) { throw new InvalidOperationException("Unexpected grpc-status value: " + grpcStatus); } + string grpcMessage = null; + if (httpResponseMessage.TrailingHeaders.TryGetValues(GrpcProtocolConstants.MessageTrailer, out var grpcMessageValues)) + { + grpcMessage = grpcMessageValues.FirstOrDefault(); + } + return new Status((StatusCode)statusValue, grpcMessage); } diff --git a/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolConstants.cs b/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolConstants.cs index d786d2322..d36f00990 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolConstants.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolConstants.cs @@ -24,6 +24,13 @@ internal static class GrpcProtocolConstants { internal const string GrpcContentType = "application/grpc"; internal static readonly MediaTypeHeaderValue GrpcContentTypeHeaderValue = new MediaTypeHeaderValue("application/grpc"); + internal static readonly EmptyContent EmptyGrpcContent; + + static GrpcProtocolConstants() + { + EmptyGrpcContent = new EmptyContent(); + EmptyGrpcContent.Headers.ContentType = GrpcContentTypeHeaderValue; + } internal const string TimeoutHeader = "grpc-timeout"; internal const string MessageEncodingHeader = "grpc-encoding"; diff --git a/src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs b/src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs index 8c8488800..14f8912e8 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs @@ -28,6 +28,7 @@ namespace Grpc.NetCore.HttpClient.Internal internal class HttpContextClientStreamReader : IAsyncStreamReader { private readonly GrpcCall _call; + private HttpResponseMessage _httpResponse; private Stream _responseStream; public HttpContextClientStreamReader(GrpcCall call) @@ -66,10 +67,13 @@ public async Task MoveNext(CancellationToken cancellationToken) cancellationToken.ThrowIfCancellationRequested(); + if (_httpResponse == null) + { + _httpResponse = await _call.SendTask.ConfigureAwait(false); + } if (_responseStream == null) { - var responseMessage = await _call.SendTask.ConfigureAwait(false); - _responseStream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false); + _responseStream = await _httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); } using (cts) @@ -78,7 +82,7 @@ public async Task MoveNext(CancellationToken cancellationToken) if (Current == null) { // No more content in response so mark as finished - _call.ResponseFinished = true; + _call.FinishResponse(_httpResponse); return false; } diff --git a/test/Grpc.NetCore.HttpClient.Tests/AsyncUnaryCallTests.cs b/test/Grpc.NetCore.HttpClient.Tests/AsyncUnaryCallTests.cs index ca919e2db..9125904a4 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/AsyncUnaryCallTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/AsyncUnaryCallTests.cs @@ -101,5 +101,23 @@ public async Task AsyncUnaryCall_Success_RequestContentSent() Assert.AreEqual("World", requestMessage.Name); } + + [Test] + public void AsyncUnaryCall_NonOkStatusTrailer_ThrowRpcError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, new ByteArrayContent(Array.Empty()), StatusCode.Unimplemented); + return Task.FromResult(response); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var ex = Assert.ThrowsAsync(async () => await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest())); + + // Assert + Assert.AreEqual(StatusCode.Unimplemented, ex.StatusCode); + } } } diff --git a/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs b/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs index 175f4fac2..d378f51eb 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs @@ -61,7 +61,6 @@ public async Task AsyncUnaryCall_MessageReturned_ReturnsTrailers() // Assert Assert.AreSame(trailers1, trailers2); - Assert.AreEqual(1, trailers1.Count); Assert.AreEqual("value", trailers1.Single(t => t.Key == "custom-header").Value); } @@ -85,7 +84,6 @@ public async Task AsyncUnaryCall_HeadersReturned_ReturnsTrailers() var trailers = call.GetTrailers(); // Assert - Assert.AreEqual(1, trailers.Count); Assert.AreEqual("value", trailers.Single(t => t.Key == "custom-header").Value); } @@ -236,7 +234,6 @@ public async Task AsyncServerStreamingCall_FinishedReader_ReturnsTrailers() var trailers = call.GetTrailers(); // Assert - Assert.AreEqual(1, trailers.Count); Assert.AreEqual("value", trailers.Single(t => t.Key == "custom-header").Value); } @@ -277,7 +274,6 @@ public async Task AsyncClientStreamingCall_CompleteWriter_ReturnsTrailers() var trailers = call.GetTrailers(); // Assert - Assert.AreEqual(1, trailers.Count); Assert.AreEqual("value", trailers.Single(t => t.Key == "custom-header").Value); } diff --git a/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/ResponseUtils.cs b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/ResponseUtils.cs index 972252ef3..e28e94cd4 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/ResponseUtils.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/ResponseUtils.cs @@ -24,6 +24,8 @@ using System.Threading; using System.Diagnostics; using System.Buffers.Binary; +using Grpc.NetCore.HttpClient.Internal; +using Grpc.Core; namespace Grpc.NetCore.HttpClient.Tests.Infrastructure { @@ -38,12 +40,16 @@ public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, stri public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, byte[] payload) => CreateResponse(statusCode, new ByteArrayContent(payload)); - public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, HttpContent payload) + public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, HttpContent payload, StatusCode grpcStatusCode = StatusCode.OK) { - return new HttpResponseMessage(statusCode) + var message = new HttpResponseMessage(statusCode) { Content = payload }; + + message.TrailingHeaders.Add(GrpcProtocolConstants.StatusTrailer, grpcStatusCode.ToString("D")); + + return message; } private const int MessageDelimiterSize = 4; // how many bytes it takes to encode "Message-Length" diff --git a/testassets/FunctionalTestsWebsite/Startup.cs b/testassets/FunctionalTestsWebsite/Startup.cs index 8abffa8e3..b260a20e6 100644 --- a/testassets/FunctionalTestsWebsite/Startup.cs +++ b/testassets/FunctionalTestsWebsite/Startup.cs @@ -67,7 +67,6 @@ public void ConfigureServices(IServiceCollection services) policy.RequireClaim(ClaimTypes.NameIdentifier); }); }); - services.AddAuthorizationPolicyEvaluator(); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { diff --git a/testassets/InteropTestsClient/Certs/README.md b/testassets/InteropTestsClient/Certs/README.md new file mode 100644 index 000000000..79206a486 --- /dev/null +++ b/testassets/InteropTestsClient/Certs/README.md @@ -0,0 +1,8 @@ +Keys taken from https://github.com/grpc/grpc/tree/master/src/core/tsi/test_creds +so that interop server in this project is compatible with interop clients +implemented in other gRPC languages. + +The server1.pem and server1.key were combined into server1.pfx. The password is 1111. These certs are not secure, do not use in production. +``` +openssl pkcs12 -export -out server1.pfx -inkey server1.key -in server1.pem -certfile ca.pem +``` diff --git a/testassets/InteropTestsClient/Certs/ca.pem b/testassets/InteropTestsClient/Certs/ca.pem new file mode 100644 index 000000000..6c8511a73 --- /dev/null +++ b/testassets/InteropTestsClient/Certs/ca.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla +Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0 +YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT +BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7 ++L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu +g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd +Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau +sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m +oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG +Dfcog5wrJytaQ6UA0wE= +-----END CERTIFICATE----- diff --git a/testassets/InteropTestsClient/Certs/server1.key b/testassets/InteropTestsClient/Certs/server1.key new file mode 100644 index 000000000..143a5b876 --- /dev/null +++ b/testassets/InteropTestsClient/Certs/server1.key @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAOHDFScoLCVJpYDD +M4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1BgzkWF+slf +3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd9N8YwbBY +AckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAECgYAn7qGnM2vbjJNBm0VZCkOkTIWm +V10okw7EPJrdL2mkre9NasghNXbE1y5zDshx5Nt3KsazKOxTT8d0Jwh/3KbaN+YY +tTCbKGW0pXDRBhwUHRcuRzScjli8Rih5UOCiZkhefUTcRb6xIhZJuQy71tjaSy0p +dHZRmYyBYO2YEQ8xoQJBAPrJPhMBkzmEYFtyIEqAxQ/o/A6E+E4w8i+KM7nQCK7q +K4JXzyXVAjLfyBZWHGM2uro/fjqPggGD6QH1qXCkI4MCQQDmdKeb2TrKRh5BY1LR +81aJGKcJ2XbcDu6wMZK4oqWbTX2KiYn9GB0woM6nSr/Y6iy1u145YzYxEV/iMwff +DJULAkB8B2MnyzOg0pNFJqBJuH29bKCcHa8gHJzqXhNO5lAlEbMK95p/P2Wi+4Hd +aiEIAF1BF326QJcvYKmwSmrORp85AkAlSNxRJ50OWrfMZnBgzVjDx3xG6KsFQVk2 +ol6VhqL6dFgKUORFUWBvnKSyhjJxurlPEahV6oo6+A+mPhFY8eUvAkAZQyTdupP3 +XEFQKctGz+9+gKkemDp7LBBMEMBXrGTLPhpEfcjv/7KPdnFHYmhYeBTBnuVmTVWe +F98XJ7tIFfJq +-----END PRIVATE KEY----- diff --git a/testassets/InteropTestsClient/Certs/server1.pem b/testassets/InteropTestsClient/Certs/server1.pem new file mode 100644 index 000000000..f3d43fcc5 --- /dev/null +++ b/testassets/InteropTestsClient/Certs/server1.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICnDCCAgWgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTET +MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ +dHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2EwHhcNMTUxMTA0MDIyMDI0WhcNMjUxMTAx +MDIyMDI0WjBlMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNV +BAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4YW1wbGUsIENvLjEaMBgGA1UEAxQRKi50 +ZXN0Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOHDFSco +LCVJpYDDM4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1Bg +zkWF+slf3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd +9N8YwbBYAckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAGjazBpMAkGA1UdEwQCMAAw +CwYDVR0PBAQDAgXgME8GA1UdEQRIMEaCECoudGVzdC5nb29nbGUuZnKCGHdhdGVy +em9vaS50ZXN0Lmdvb2dsZS5iZYISKi50ZXN0LnlvdXR1YmUuY29thwTAqAEDMA0G +CSqGSIb3DQEBCwUAA4GBAJFXVifQNub1LUP4JlnX5lXNlo8FxZ2a12AFQs+bzoJ6 +hM044EDjqyxUqSbVePK0ni3w1fHQB5rY9yYC5f8G7aqqTY1QOhoUk8ZTSTRpnkTh +y4jjdvTZeLDVBlueZUTDRmy2feY5aZIU18vFDK08dTG0A87pppuv1LNIR3loveU8 +-----END CERTIFICATE----- diff --git a/testassets/InteropTestsClient/Certs/server1.pfx b/testassets/InteropTestsClient/Certs/server1.pfx new file mode 100644 index 0000000000000000000000000000000000000000..252acc126d589e050a15e582a2a5fa678ff5dd8e GIT binary patch literal 2325 zcmY+EX*3j!8pq9IFpPv0k!Ebegpswe?}Q8nku?d~#cRuQhX#$M?8cfcvXgzy5)mzv zL5!VjS(7NdI_KW|-uvM>&pE&Ud7e-I^F!kq&I17;G@gD8!XQG>AsoN}bbvfO{QwwG z-+P9;(0JO;e-4ZXRGL+1c2s-K>z)L2>?MegXv{?jRn{>;>v+QdYW)N zZ9wQ?EX_^PM&r>8Xd4c>&#>xLa%$}Wgdcp;vGbMe ztsC#CC6Y9OaLaCHV4KSj9H@<<4%A0A3nO_za9rUv+VTz-C%&3=?_dH?K+cFiU*mh! z&hZ14c`G!YRH%Zg)@ckIG8#_`fT{y*_7@f7oXmD#umCn^|&5@W0@EtXY8Zkc4~JcFcwhiZsx}gf$D${AS`L%@vx3TBLKG z+{xU#viNGF4}OvY@77SK%%jIsWL6pI)v|6F0tQF{T=00$VQYTftbQxSHa~gHeXN77 z`2f7NFI7u$n#{nfnOc}fN!*fkEzGLh8pWyU7X!(AoQptyicK`2IZ&|*q;rZL{d z)HS;|`Ox;OEU2>9g+z*t^I{Y}-EEU!Jl?Vk7RYSh94lFFRP-qMsHMT*wGgB9`1|qB zfX0CvJ+f$bm2v)uCJ0h>vU%J(M8-VDP&-MrPOtAr+j=U_RbR}B7V?Vxp*Aof&9h)r4t#WeK5jnl`Ren%z&e*YxrESP1&ITD;(=vqhQ z*7nc8H+JcRa2GseUe;r{%ZA+%e5~3-#d}<)j3Q7Xp)a`|DuPrWc%v|C8t%qbRfo6a zo_wvju$NY=^x>QX-^>&_PPkeu9H2=k>sjwvfZtvjp#KQ zlA5d}@#w1SA{ka?K>JJn!X3}hj?%1|1AdaDNAX?lU0cC#9?=MoqIvbCwXP+t?aX|-M>?EauEJchZx z<5hOQHp~PXAQq4tQKY-@tevQk6^5ezG@tye#hv-k)rrB#VX{Bk_T9oNFTzknCrb6 zvmSsY6=<%W@ZGeYl5o!RC|5g0HeeR^d4iZ$vLP3=kZ?$z5-}~}nmx&=JQSsh*Q6K7?wcz?9h>wwycGJoJ2<9mGfT|9!x0N`fDRkLPE0Qf4Q+@^%uuYyheXK%I_A#1CEc_ zUVF?FXwcX%z3SC2qhZKj>E!C(#0f0V)ajhf4b-NjQq3FAa5huQT3tLMi@ul{68Cwj zF9yETjgxuBQ>mC?@~(n+26dUV^*C^&O{A&II9YePMdoC7YFvi0yn1Or%s!85$2f{2x*0 z;el&lJaG98&!5dSgz-Pxh5~?RVeuKv|3909{@9%V;o-$cUH&eAY<_kg{xp&AY1Hk& z%Q-DPha)=C%}j}{k4V;-7i$#!Ad24Hcz%uiR3Yt zwBS#Evg9+H&ar)6TCQu%NX*LRqN?7TzjEn_;4D|5c9gMOm3OSws{My_>x=5(lPAjsC)htO*Ca&*ymvX;T9x-M}ZwwThB9`%+1{TiLU%iww^iB{t z{Lvt-Bl&mFZ7Gogq58*}*!2p37a_5m*@4e3g~%3B8A6;R7M1ssiV9Z4PAjj9X(qKN zB_uoDZ|mGp=7}TY$pW1rT+;?a!Cby+*4+coCa;*u#ey^?@}PC9Id}HH(VyyEMv-u+--VZWIZS^db`qrTDBKV8Z3Be0!QOBR#`{FVH z8Aff6cp426^|IQMT<%BtvkB3czOX=wWn1c^Dbo<2bA#(p?CdE8-MhHVB1-Oyd4@Q} z-8{zOro&4l*pRBMg*C%c?C=!zr!8f~o!08^cI($hL%?Tk{j=B0+9gP00gJl7@4hN# zl!doNn7V~mi7v`0*L!|(o*Cd51cV!n&ZJlI-*;A1=GV#CKS=5!(Y^rKt1(D>0Qvpy zt2Q)SQ&eEmx#x#8e_NOGy!2!_B!l`Ba5yd6oZm z3T@xb;4HZr-Q8pg&~o>Ouk#6bqy_Bfed<|%l*kAxciyyFfSZ3OwsoZacB<*c+7L`N z_7iL8UytgINC=?${@ZGX3lbb)_E(D5`*aEK^3thH(#+D!w86uu3bX_oj%I++qL@KI qE?OEeyK1<&M%1+lOL6(xCVM3W`G!8Q5=>8Xs9PHE8$tTh?!N(5%tQqM literal 0 HcmV?d00001 diff --git a/testassets/InteropTestsClient/IChannel.cs b/testassets/InteropTestsClient/IChannel.cs new file mode 100644 index 000000000..9e5611e44 --- /dev/null +++ b/testassets/InteropTestsClient/IChannel.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Grpc.Core; + +namespace InteropTestsClient +{ + public interface IChannel + { + Task ShutdownAsync(); + } + + public class HttpClientChannel : IChannel + { + public Task ShutdownAsync() + { + return Task.CompletedTask; + } + } + + public class CoreChannel : IChannel + { + public Channel Channel { get; } + + public CoreChannel(Channel channel) + { + Channel = channel; + } + + public Task ShutdownAsync() + { + return Channel.ShutdownAsync(); + } + } +} diff --git a/testassets/InteropTestsClient/InteropClient.cs b/testassets/InteropTestsClient/InteropClient.cs new file mode 100644 index 000000000..b8e5b959c --- /dev/null +++ b/testassets/InteropTestsClient/InteropClient.cs @@ -0,0 +1,749 @@ +#region Copyright notice and license + +// Copyright 2015-2016 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommandLine; +using Google.Apis.Auth.OAuth2; +using Google.Protobuf; +using Grpc.Auth; +using Grpc.Core; +using Grpc.Core.Logging; +using Grpc.Core.Utils; +using Grpc.NetCore.HttpClient; +using Grpc.Testing; +using Newtonsoft.Json.Linq; +using NUnit.Framework; + +namespace InteropTestsClient +{ + public class InteropClient + { + internal const string CompressionRequestAlgorithmMetadataKey = "grpc-internal-encoding-request"; + + private class ClientOptions + { + [Option("client_type", Default = "httpclient")] + public string ClientType { get; set; } + + [Option("server_host", Default = "localhost")] + public string ServerHost { get; set; } + + [Option("server_host_override")] + public string ServerHostOverride { get; set; } + + [Option("server_port", Default = 50052)] + public int ServerPort { get; set; } + + [Option("test_case", Default = "unimplemented_method")] + public string TestCase { get; set; } + + // Deliberately using nullable bool type to allow --use_tls=true syntax (as opposed to --use_tls) + [Option("use_tls", Default = false)] + public bool? UseTls { get; set; } + + // Deliberately using nullable bool type to allow --use_test_ca=true syntax (as opposed to --use_test_ca) + [Option("use_test_ca", Default = false)] + public bool? UseTestCa { get; set; } + + [Option("default_service_account", Required = false)] + public string DefaultServiceAccount { get; set; } + + [Option("oauth_scope", Required = false)] + public string OAuthScope { get; set; } + + [Option("service_account_key_file", Required = false)] + public string ServiceAccountKeyFile { get; set; } + } + + ClientOptions options; + + private InteropClient(ClientOptions options) + { + this.options = options; + } + + public static void Run(string[] args) + { + GrpcEnvironment.SetLogger(new ConsoleLogger()); + var parserResult = Parser.Default.ParseArguments(args) + .WithNotParsed(errors => Environment.Exit(1)) + .WithParsed(options => + { + Console.WriteLine("Use TLS: " + options.UseTls); + Console.WriteLine("Server host: " + options.ServerHost); + Console.WriteLine("Server port: " + options.ServerPort); + + var interopClient = new InteropClient(options); + interopClient.Run().Wait(); + }); + } + + private async Task Run() + { + IChannel channel = IsHttpClient() ? await HttpClientCreateChannel() : await CoreCreateChannel(); + await RunTestCaseAsync(channel, options); + await channel.ShutdownAsync(); + } + + private Task HttpClientCreateChannel() + { + return Task.FromResult(new HttpClientChannel()); + } + + private async Task CoreCreateChannel() + { + var credentials = await CreateCredentialsAsync(); + + List channelOptions = null; + if (!string.IsNullOrEmpty(options.ServerHostOverride)) + { + channelOptions = new List + { + new ChannelOption(ChannelOptions.SslTargetNameOverride, options.ServerHostOverride) + }; + } + var channel = new Channel(options.ServerHost, options.ServerPort, credentials, channelOptions); + return new CoreChannel(channel); + } + + private bool IsHttpClient() => string.Equals(options.ClientType, "httpclient", StringComparison.OrdinalIgnoreCase); + + private async Task CreateCredentialsAsync() + { + var credentials = ChannelCredentials.Insecure; + if (options.UseTls.Value) + { + credentials = options.UseTestCa.Value ? TestCredentials.CreateSslCredentials() : new SslCredentials(); + } + + if (options.TestCase == "jwt_token_creds") + { + var googleCredential = await GoogleCredential.GetApplicationDefaultAsync(); + Assert.IsTrue(googleCredential.IsCreateScopedRequired); + credentials = ChannelCredentials.Create(credentials, googleCredential.ToCallCredentials()); + } + + if (options.TestCase == "compute_engine_creds") + { + var googleCredential = await GoogleCredential.GetApplicationDefaultAsync(); + Assert.IsFalse(googleCredential.IsCreateScopedRequired); + credentials = ChannelCredentials.Create(credentials, googleCredential.ToCallCredentials()); + } + return credentials; + } + + private TClient CreateClient(IChannel channel) where TClient : ClientBase + { + if (channel is CoreChannel coreChannel) + { + return (TClient)Activator.CreateInstance(typeof(TClient), coreChannel.Channel); + } + else + { + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + return GrpcClientFactory.Create($"http://{options.ServerHost}:{options.ServerPort}"); + } + } + + private async Task RunTestCaseAsync(IChannel channel, ClientOptions options) + { + var client = CreateClient(channel); + switch (options.TestCase) + { + case "empty_unary": + RunEmptyUnary(client); + break; + case "large_unary": + RunLargeUnary(client); + break; + case "client_streaming": + await RunClientStreamingAsync(client); + break; + case "server_streaming": + await RunServerStreamingAsync(client); + break; + case "ping_pong": + await RunPingPongAsync(client); + break; + case "empty_stream": + await RunEmptyStreamAsync(client); + break; + case "compute_engine_creds": + RunComputeEngineCreds(client, options.DefaultServiceAccount, options.OAuthScope); + break; + case "jwt_token_creds": + RunJwtTokenCreds(client); + break; + case "oauth2_auth_token": + await RunOAuth2AuthTokenAsync(client, options.OAuthScope); + break; + case "per_rpc_creds": + await RunPerRpcCredsAsync(client, options.OAuthScope); + break; + case "cancel_after_begin": + await RunCancelAfterBeginAsync(client); + break; + case "cancel_after_first_response": + await RunCancelAfterFirstResponseAsync(client); + break; + case "timeout_on_sleeping_server": + await RunTimeoutOnSleepingServerAsync(client); + break; + case "custom_metadata": + await RunCustomMetadataAsync(client); + break; + case "status_code_and_message": + await RunStatusCodeAndMessageAsync(client); + break; + case "unimplemented_service": + RunUnimplementedService(CreateClient(channel)); + break; + case "unimplemented_method": + RunUnimplementedMethod(client); + break; + case "client_compressed_unary": + RunClientCompressedUnary(client); + break; + case "client_compressed_streaming": + await RunClientCompressedStreamingAsync(client); + break; + default: + throw new ArgumentException("Unknown test case " + options.TestCase); + } + } + + public static void RunEmptyUnary(TestService.TestServiceClient client) + { + Console.WriteLine("running empty_unary"); + var response = client.EmptyCall(new Empty()); + Assert.IsNotNull(response); + Console.WriteLine("Passed!"); + } + + public static void RunLargeUnary(TestService.TestServiceClient client) + { + Console.WriteLine("running large_unary"); + var request = new SimpleRequest + { + ResponseSize = 314159, + Payload = CreateZerosPayload(271828) + }; + var response = client.UnaryCall(request); + + Assert.AreEqual(314159, response.Payload.Body.Length); + Console.WriteLine("Passed!"); + } + + public static async Task RunClientStreamingAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running client_streaming"); + + var bodySizes = new List { 27182, 8, 1828, 45904 }.Select((size) => new StreamingInputCallRequest { Payload = CreateZerosPayload(size) }); + + using (var call = client.StreamingInputCall()) + { + await call.RequestStream.WriteAllAsync(bodySizes); + + var response = await call.ResponseAsync; + Assert.AreEqual(74922, response.AggregatedPayloadSize); + } + Console.WriteLine("Passed!"); + } + + public static async Task RunServerStreamingAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running server_streaming"); + + var bodySizes = new List { 31415, 9, 2653, 58979 }; + + var request = new StreamingOutputCallRequest + { + ResponseParameters = { bodySizes.Select((size) => new ResponseParameters { Size = size }) } + }; + + using (var call = client.StreamingOutputCall(request)) + { + var responseList = await call.ResponseStream.ToListAsync(); + CollectionAssert.AreEqual(bodySizes, responseList.Select((item) => item.Payload.Body.Length)); + } + Console.WriteLine("Passed!"); + } + + public static async Task RunPingPongAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running ping_pong"); + + using (var call = client.FullDuplexCall()) + { + await call.RequestStream.WriteAsync(new StreamingOutputCallRequest + { + ResponseParameters = { new ResponseParameters { Size = 31415 } }, + Payload = CreateZerosPayload(27182) + }); + + Assert.IsTrue(await call.ResponseStream.MoveNext()); + Assert.AreEqual(31415, call.ResponseStream.Current.Payload.Body.Length); + + await call.RequestStream.WriteAsync(new StreamingOutputCallRequest + { + ResponseParameters = { new ResponseParameters { Size = 9 } }, + Payload = CreateZerosPayload(8) + }); + + Assert.IsTrue(await call.ResponseStream.MoveNext()); + Assert.AreEqual(9, call.ResponseStream.Current.Payload.Body.Length); + + await call.RequestStream.WriteAsync(new StreamingOutputCallRequest + { + ResponseParameters = { new ResponseParameters { Size = 2653 } }, + Payload = CreateZerosPayload(1828) + }); + + Assert.IsTrue(await call.ResponseStream.MoveNext()); + Assert.AreEqual(2653, call.ResponseStream.Current.Payload.Body.Length); + + await call.RequestStream.WriteAsync(new StreamingOutputCallRequest + { + ResponseParameters = { new ResponseParameters { Size = 58979 } }, + Payload = CreateZerosPayload(45904) + }); + + Assert.IsTrue(await call.ResponseStream.MoveNext()); + Assert.AreEqual(58979, call.ResponseStream.Current.Payload.Body.Length); + + await call.RequestStream.CompleteAsync(); + + Assert.IsFalse(await call.ResponseStream.MoveNext()); + } + Console.WriteLine("Passed!"); + } + + public static async Task RunEmptyStreamAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running empty_stream"); + using (var call = client.FullDuplexCall()) + { + await call.RequestStream.CompleteAsync(); + + var responseList = await call.ResponseStream.ToListAsync(); + Assert.AreEqual(0, responseList.Count); + } + Console.WriteLine("Passed!"); + } + + public static void RunComputeEngineCreds(TestService.TestServiceClient client, string defaultServiceAccount, string oauthScope) + { + Console.WriteLine("running compute_engine_creds"); + + var request = new SimpleRequest + { + ResponseSize = 314159, + Payload = CreateZerosPayload(271828), + FillUsername = true, + FillOauthScope = true + }; + + // not setting credentials here because they were set on channel already + var response = client.UnaryCall(request); + + Assert.AreEqual(314159, response.Payload.Body.Length); + Assert.False(string.IsNullOrEmpty(response.OauthScope)); + Assert.True(oauthScope.Contains(response.OauthScope)); + Assert.AreEqual(defaultServiceAccount, response.Username); + Console.WriteLine("Passed!"); + } + + public static void RunJwtTokenCreds(TestService.TestServiceClient client) + { + Console.WriteLine("running jwt_token_creds"); + + var request = new SimpleRequest + { + ResponseSize = 314159, + Payload = CreateZerosPayload(271828), + FillUsername = true, + }; + + // not setting credentials here because they were set on channel already + var response = client.UnaryCall(request); + + Assert.AreEqual(314159, response.Payload.Body.Length); + Assert.AreEqual(GetEmailFromServiceAccountFile(), response.Username); + Console.WriteLine("Passed!"); + } + + public static async Task RunOAuth2AuthTokenAsync(TestService.TestServiceClient client, string oauthScope) + { + Console.WriteLine("running oauth2_auth_token"); + ITokenAccess credential = (await GoogleCredential.GetApplicationDefaultAsync()).CreateScoped(new[] { oauthScope }); + string oauth2Token = await credential.GetAccessTokenForRequestAsync(); + + var credentials = GoogleGrpcCredentials.FromAccessToken(oauth2Token); + var request = new SimpleRequest + { + FillUsername = true, + FillOauthScope = true + }; + + var response = client.UnaryCall(request, new CallOptions(credentials: credentials)); + + Assert.False(string.IsNullOrEmpty(response.OauthScope)); + Assert.True(oauthScope.Contains(response.OauthScope)); + Assert.AreEqual(GetEmailFromServiceAccountFile(), response.Username); + Console.WriteLine("Passed!"); + } + + public static async Task RunPerRpcCredsAsync(TestService.TestServiceClient client, string oauthScope) + { + Console.WriteLine("running per_rpc_creds"); + ITokenAccess googleCredential = await GoogleCredential.GetApplicationDefaultAsync(); + + var credentials = googleCredential.ToCallCredentials(); + var request = new SimpleRequest + { + FillUsername = true, + }; + + var response = client.UnaryCall(request, new CallOptions(credentials: credentials)); + + Assert.AreEqual(GetEmailFromServiceAccountFile(), response.Username); + Console.WriteLine("Passed!"); + } + + public static async Task RunCancelAfterBeginAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running cancel_after_begin"); + + var cts = new CancellationTokenSource(); + using (var call = client.StreamingInputCall(cancellationToken: cts.Token)) + { + // TODO(jtattermusch): we need this to ensure call has been initiated once we cancel it. + await Task.Delay(1000); + cts.Cancel(); + + var ex = Assert.ThrowsAsync(async () => await call.ResponseAsync); + Assert.AreEqual(StatusCode.Cancelled, ex.Status.StatusCode); + } + Console.WriteLine("Passed!"); + } + + public static async Task RunCancelAfterFirstResponseAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running cancel_after_first_response"); + + var cts = new CancellationTokenSource(); + using (var call = client.FullDuplexCall(cancellationToken: cts.Token)) + { + await call.RequestStream.WriteAsync(new StreamingOutputCallRequest + { + ResponseParameters = { new ResponseParameters { Size = 31415 } }, + Payload = CreateZerosPayload(27182) + }); + + Assert.IsTrue(await call.ResponseStream.MoveNext()); + Assert.AreEqual(31415, call.ResponseStream.Current.Payload.Body.Length); + + cts.Cancel(); + + try + { + // cannot use Assert.ThrowsAsync because it uses Task.Wait and would deadlock. + await call.ResponseStream.MoveNext(); + Assert.Fail(); + } + catch (RpcException ex) + { + Assert.AreEqual(StatusCode.Cancelled, ex.Status.StatusCode); + } + } + Console.WriteLine("Passed!"); + } + + public static async Task RunTimeoutOnSleepingServerAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running timeout_on_sleeping_server"); + + var deadline = DateTime.UtcNow.AddMilliseconds(1); + using (var call = client.FullDuplexCall(deadline: deadline)) + { + try + { + await call.RequestStream.WriteAsync(new StreamingOutputCallRequest { Payload = CreateZerosPayload(27182) }); + } + catch (InvalidOperationException) + { + // Deadline was reached before write has started. Eat the exception and continue. + } + catch (RpcException) + { + // Deadline was reached before write has started. Eat the exception and continue. + } + + try + { + await call.ResponseStream.MoveNext(); + Assert.Fail(); + } + catch (RpcException ex) + { + // We can't guarantee the status code always DeadlineExceeded. See issue #2685. + Assert.Contains(ex.Status.StatusCode, new[] { StatusCode.DeadlineExceeded, StatusCode.Internal }); + } + } + Console.WriteLine("Passed!"); + } + + public static async Task RunCustomMetadataAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running custom_metadata"); + { + // step 1: test unary call + var request = new SimpleRequest + { + ResponseSize = 314159, + Payload = CreateZerosPayload(271828) + }; + + var call = client.UnaryCallAsync(request, headers: CreateTestMetadata()); + await call.ResponseAsync; + + var responseHeaders = await call.ResponseHeadersAsync; + var responseTrailers = call.GetTrailers(); + + Assert.AreEqual("test_initial_metadata_value", responseHeaders.First((entry) => entry.Key == "x-grpc-test-echo-initial").Value); + CollectionAssert.AreEqual(new byte[] { 0xab, 0xab, 0xab }, responseTrailers.First((entry) => entry.Key == "x-grpc-test-echo-trailing-bin").ValueBytes); + } + + { + // step 2: test full duplex call + var request = new StreamingOutputCallRequest + { + ResponseParameters = { new ResponseParameters { Size = 31415 } }, + Payload = CreateZerosPayload(27182) + }; + + var call = client.FullDuplexCall(headers: CreateTestMetadata()); + + await call.RequestStream.WriteAsync(request); + await call.RequestStream.CompleteAsync(); + await call.ResponseStream.ToListAsync(); + + var responseHeaders = await call.ResponseHeadersAsync; + var responseTrailers = call.GetTrailers(); + + Assert.AreEqual("test_initial_metadata_value", responseHeaders.First((entry) => entry.Key == "x-grpc-test-echo-initial").Value); + CollectionAssert.AreEqual(new byte[] { 0xab, 0xab, 0xab }, responseTrailers.First((entry) => entry.Key == "x-grpc-test-echo-trailing-bin").ValueBytes); + } + + Console.WriteLine("Passed!"); + } + + public static async Task RunStatusCodeAndMessageAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running status_code_and_message"); + var echoStatus = new EchoStatus + { + Code = 2, + Message = "test status message" + }; + + { + // step 1: test unary call + var request = new SimpleRequest { ResponseStatus = echoStatus }; + + var e = Assert.Throws(() => client.UnaryCall(request)); + Assert.AreEqual(StatusCode.Unknown, e.Status.StatusCode); + Assert.AreEqual(echoStatus.Message, e.Status.Detail); + } + + { + // step 2: test full duplex call + var request = new StreamingOutputCallRequest { ResponseStatus = echoStatus }; + + var call = client.FullDuplexCall(); + await call.RequestStream.WriteAsync(request); + await call.RequestStream.CompleteAsync(); + + try + { + // cannot use Assert.ThrowsAsync because it uses Task.Wait and would deadlock. + await call.ResponseStream.ToListAsync(); + Assert.Fail(); + } + catch (RpcException e) + { + Assert.AreEqual(StatusCode.Unknown, e.Status.StatusCode); + Assert.AreEqual(echoStatus.Message, e.Status.Detail); + } + } + + Console.WriteLine("Passed!"); + } + + public static void RunUnimplementedService(UnimplementedService.UnimplementedServiceClient client) + { + Console.WriteLine("running unimplemented_service"); + var e = Assert.Throws(() => client.UnimplementedCall(new Empty())); + + Assert.AreEqual(StatusCode.Unimplemented, e.Status.StatusCode); + Console.WriteLine("Passed!"); + } + + public static void RunUnimplementedMethod(TestService.TestServiceClient client) + { + Console.WriteLine("running unimplemented_method"); + var e = Assert.Throws(() => client.UnimplementedCall(new Empty())); + + Assert.AreEqual(StatusCode.Unimplemented, e.Status.StatusCode); + Console.WriteLine("Passed!"); + } + + public static void RunClientCompressedUnary(TestService.TestServiceClient client) + { + Console.WriteLine("running client_compressed_unary"); + var probeRequest = new SimpleRequest + { + ExpectCompressed = new BoolValue + { + Value = true // lie about compression + }, + ResponseSize = 314159, + Payload = CreateZerosPayload(271828) + }; + var e = Assert.Throws(() => client.UnaryCall(probeRequest, CreateClientCompressionMetadata(false))); + Assert.AreEqual(StatusCode.InvalidArgument, e.Status.StatusCode); + + var compressedRequest = new SimpleRequest + { + ExpectCompressed = new BoolValue + { + Value = true + }, + ResponseSize = 314159, + Payload = CreateZerosPayload(271828) + }; + var response1 = client.UnaryCall(compressedRequest, CreateClientCompressionMetadata(true)); + Assert.AreEqual(314159, response1.Payload.Body.Length); + + var uncompressedRequest = new SimpleRequest + { + ExpectCompressed = new BoolValue + { + Value = false + }, + ResponseSize = 314159, + Payload = CreateZerosPayload(271828) + }; + var response2 = client.UnaryCall(uncompressedRequest, CreateClientCompressionMetadata(false)); + Assert.AreEqual(314159, response2.Payload.Body.Length); + + Console.WriteLine("Passed!"); + } + + public static async Task RunClientCompressedStreamingAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running client_compressed_streaming"); + try + { + var probeCall = client.StreamingInputCall(CreateClientCompressionMetadata(false)); + await probeCall.RequestStream.WriteAsync(new StreamingInputCallRequest + { + ExpectCompressed = new BoolValue + { + Value = true + }, + Payload = CreateZerosPayload(27182) + }); + + // cannot use Assert.ThrowsAsync because it uses Task.Wait and would deadlock. + await probeCall; + Assert.Fail(); + } + catch (RpcException e) + { + Assert.AreEqual(StatusCode.InvalidArgument, e.Status.StatusCode); + } + + var call = client.StreamingInputCall(CreateClientCompressionMetadata(true)); + await call.RequestStream.WriteAsync(new StreamingInputCallRequest + { + ExpectCompressed = new BoolValue + { + Value = true + }, + Payload = CreateZerosPayload(27182) + }); + + call.RequestStream.WriteOptions = new WriteOptions(WriteFlags.NoCompress); + await call.RequestStream.WriteAsync(new StreamingInputCallRequest + { + ExpectCompressed = new BoolValue + { + Value = false + }, + Payload = CreateZerosPayload(45904) + }); + await call.RequestStream.CompleteAsync(); + + var response = await call.ResponseAsync; + Assert.AreEqual(73086, response.AggregatedPayloadSize); + + Console.WriteLine("Passed!"); + } + + private static Payload CreateZerosPayload(int size) + { + return new Payload { Body = ByteString.CopyFrom(new byte[size]) }; + } + + private static Metadata CreateClientCompressionMetadata(bool compressed) + { + var algorithmName = compressed ? "gzip" : "identity"; + return new Metadata + { + { new Metadata.Entry(CompressionRequestAlgorithmMetadataKey, algorithmName) } + }; + } + + // extracts the client_email field from service account file used for auth test cases + private static string GetEmailFromServiceAccountFile() + { + string keyFile = Environment.GetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS"); + Assert.IsNotNull(keyFile); + var jobject = JObject.Parse(File.ReadAllText(keyFile)); + string email = jobject.GetValue("client_email").Value(); + Assert.IsTrue(email.Length > 0); // spec requires nonempty client email. + return email; + } + + private static Metadata CreateTestMetadata() + { + return new Metadata + { + {"x-grpc-test-echo-initial", "test_initial_metadata_value"}, + {"x-grpc-test-echo-trailing-bin", new byte[] {0xab, 0xab, 0xab}} + }; + } + } +} \ No newline at end of file diff --git a/testassets/InteropTestsClient/InteropTestsClient.csproj b/testassets/InteropTestsClient/InteropTestsClient.csproj new file mode 100644 index 000000000..55d7f44be --- /dev/null +++ b/testassets/InteropTestsClient/InteropTestsClient.csproj @@ -0,0 +1,24 @@ + + + + Exe + netcoreapp3.0 + + + + + + + + + + + + + + + + + + + diff --git a/testassets/InteropTestsClient/Program.cs b/testassets/InteropTestsClient/Program.cs new file mode 100644 index 000000000..3e204d853 --- /dev/null +++ b/testassets/InteropTestsClient/Program.cs @@ -0,0 +1,28 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +namespace InteropTestsClient +{ + public class Program + { + public static void Main(string[] args) + { + InteropClient.Run(args); + } + } +} diff --git a/testassets/InteropTestsClient/RunTests.ps1 b/testassets/InteropTestsClient/RunTests.ps1 new file mode 100644 index 000000000..f0256ce75 --- /dev/null +++ b/testassets/InteropTestsClient/RunTests.ps1 @@ -0,0 +1,35 @@ +$allTests = + "empty_unary", + "large_unary", + "client_streaming", + "server_streaming", + #"ping_pong", + "empty_stream", + + #"compute_engine_creds", + #"jwt_token_creds", + #"oauth2_auth_token", + #"per_rpc_creds", + + #"cancel_after_begin", + #"cancel_after_first_response", + #"timeout_on_sleeping_server", + "custom_metadata", + "status_code_and_message", + "unimplemented_service", + "unimplemented_method" + #, + #"client_compressed_unary", + #"client_compressed_streaming" + +Write-Host "Running $($allTests.Count) tests" -ForegroundColor Cyan +Write-Host + +foreach ($test in $allTests) +{ + Write-Host "Running $test" -ForegroundColor Cyan + dotnet run --use_tls true --server_port 50052 --client_type HttpClient --test_case $test + Write-Host +} + +Write-Host "Done" -ForegroundColor Cyan \ No newline at end of file diff --git a/testassets/InteropTestsClient/TestCredentials.cs b/testassets/InteropTestsClient/TestCredentials.cs new file mode 100644 index 000000000..088fcd89b --- /dev/null +++ b/testassets/InteropTestsClient/TestCredentials.cs @@ -0,0 +1,75 @@ +#region Copyright notice and license + +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System.IO; +using System.Reflection; +using Grpc.Core; + +namespace InteropTestsClient +{ + /// + /// SSL Credentials for testing. + /// + public static class TestCredentials + { + public const string DefaultHostOverride = "foo.test.google.fr"; + + public static string ClientCertAuthorityPath + { + get + { + return GetPath("data/ca.pem"); + } + } + + public static string ServerCertChainPath + { + get + { + return GetPath("data/server1.pem"); + } + } + + public static string ServerPrivateKeyPath + { + get + { + return GetPath("data/server1.key"); + } + } + + public static SslCredentials CreateSslCredentials() + { + return new SslCredentials(File.ReadAllText(ClientCertAuthorityPath)); + } + + public static SslServerCredentials CreateSslServerCredentials() + { + var keyCertPair = new KeyCertificatePair( + File.ReadAllText(ServerCertChainPath), + File.ReadAllText(ServerPrivateKeyPath)); + return new SslServerCredentials(new[] { keyCertPair }); + } + + private static string GetPath(string relativePath) + { + var assemblyDir = Path.GetDirectoryName(typeof(TestCredentials).GetTypeInfo().Assembly.Location); + return Path.Combine(assemblyDir, relativePath); + } + } +} diff --git a/testassets/InteropTestsWebsite/Program.cs b/testassets/InteropTestsWebsite/Program.cs index a53520ead..7c627785b 100644 --- a/testassets/InteropTestsWebsite/Program.cs +++ b/testassets/InteropTestsWebsite/Program.cs @@ -16,6 +16,7 @@ #endregion +using System; using System.Runtime.InteropServices; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; @@ -43,6 +44,8 @@ public static IWebHostBuilder CreateWebHostBuilder(string[] args) => options.Limits.MinRequestBodyDataRate = null; options.ListenAnyIP(port, listenOptions => { + Console.WriteLine($"Enabling connection encryption: {useTls}"); + if (useTls) { listenOptions.UseHttps(Resources.ServerPFXPath, "1111"); diff --git a/testassets/InteropTestsWebsite/TestServiceImpl.cs b/testassets/InteropTestsWebsite/TestServiceImpl.cs index d9c46cb93..cfc5ab706 100644 --- a/testassets/InteropTestsWebsite/TestServiceImpl.cs +++ b/testassets/InteropTestsWebsite/TestServiceImpl.cs @@ -39,6 +39,7 @@ public override async Task UnaryCall(SimpleRequest request, Serv { await EnsureEchoMetadataAsync(context); EnsureEchoStatus(request.ResponseStatus, context); + EnsureCompression(request.ExpectCompressed, context); var response = new SimpleResponse { Payload = CreateZerosPayload(request.ResponseSize) }; return response; @@ -63,6 +64,8 @@ public override async Task StreamingInputCall(IAsync int sum = 0; await requestStream.ForEachAsync(request => { + EnsureCompression(request.ExpectCompressed, context); + sum += request.Payload.Body.Length; return Task.CompletedTask; }); @@ -116,5 +119,20 @@ private static void EnsureEchoStatus(EchoStatus responseStatus, ServerCallContex context.Status = new Status(statusCode, responseStatus.Message); } } + + private static void EnsureCompression(BoolValue expectCompressed, ServerCallContext context) + { + if (expectCompressed != null) + { + string encoding = context.RequestHeaders.SingleOrDefault(h => h.Key == "grpc-encoding")?.Value; + if (expectCompressed.Value) + { + if (encoding == null || encoding == "identity") + { + throw new RpcException(new Status(StatusCode.InvalidArgument, string.Empty)); + } + } + } + } } } diff --git a/testassets/Proto/empty.proto b/testassets/Proto/empty.proto new file mode 100644 index 000000000..6a0aa88df --- /dev/null +++ b/testassets/Proto/empty.proto @@ -0,0 +1,28 @@ + +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package grpc.testing; + +// An empty message that you can re-use to avoid defining duplicated empty +// messages in your project. A typical example is to use it as argument or the +// return value of a service API. For instance: +// +// service Foo { +// rpc Bar (grpc.testing.Empty) returns (grpc.testing.Empty) { }; +// }; +// +message Empty {} diff --git a/testassets/Proto/messages.proto b/testassets/Proto/messages.proto new file mode 100644 index 000000000..7b1b7286d --- /dev/null +++ b/testassets/Proto/messages.proto @@ -0,0 +1,165 @@ + +// Copyright 2015-2016 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Message definitions to be used by integration test service definitions. + +syntax = "proto3"; + +package grpc.testing; + +// TODO(dgq): Go back to using well-known types once +// https://github.com/grpc/grpc/issues/6980 has been fixed. +// import "google/protobuf/wrappers.proto"; +message BoolValue { + // The bool value. + bool value = 1; +} + +// The type of payload that should be returned. +enum PayloadType { + // Compressable text format. + COMPRESSABLE = 0; +} + +// A block of data, to simply increase gRPC message size. +message Payload { + // The type of data in body. + PayloadType type = 1; + // Primary contents of payload. + bytes body = 2; +} + +// A protobuf representation for grpc status. This is used by test +// clients to specify a status that the server should attempt to return. +message EchoStatus { + int32 code = 1; + string message = 2; +} + +// Unary request. +message SimpleRequest { + // Desired payload type in the response from the server. + // If response_type is RANDOM, server randomly chooses one from other formats. + PayloadType response_type = 1; + + // Desired payload size in the response from the server. + int32 response_size = 2; + + // Optional input payload sent along with the request. + Payload payload = 3; + + // Whether SimpleResponse should include username. + bool fill_username = 4; + + // Whether SimpleResponse should include OAuth scope. + bool fill_oauth_scope = 5; + + // Whether to request the server to compress the response. This field is + // "nullable" in order to interoperate seamlessly with clients not able to + // implement the full compression tests by introspecting the call to verify + // the response's compression status. + BoolValue response_compressed = 6; + + // Whether server should return a given status + EchoStatus response_status = 7; + + // Whether the server should expect this request to be compressed. + BoolValue expect_compressed = 8; +} + +// Unary response, as configured by the request. +message SimpleResponse { + // Payload to increase message size. + Payload payload = 1; + // The user the request came from, for verifying authentication was + // successful when the client expected it. + string username = 2; + // OAuth scope. + string oauth_scope = 3; +} + +// Client-streaming request. +message StreamingInputCallRequest { + // Optional input payload sent along with the request. + Payload payload = 1; + + // Whether the server should expect this request to be compressed. This field + // is "nullable" in order to interoperate seamlessly with servers not able to + // implement the full compression tests by introspecting the call to verify + // the request's compression status. + BoolValue expect_compressed = 2; + + // Not expecting any payload from the response. +} + +// Client-streaming response. +message StreamingInputCallResponse { + // Aggregated size of payloads received from the client. + int32 aggregated_payload_size = 1; +} + +// Configuration for a particular response. +message ResponseParameters { + // Desired payload sizes in responses from the server. + int32 size = 1; + + // Desired interval between consecutive responses in the response stream in + // microseconds. + int32 interval_us = 2; + + // Whether to request the server to compress the response. This field is + // "nullable" in order to interoperate seamlessly with clients not able to + // implement the full compression tests by introspecting the call to verify + // the response's compression status. + BoolValue compressed = 3; +} + +// Server-streaming request. +message StreamingOutputCallRequest { + // Desired payload type in the response from the server. + // If response_type is RANDOM, the payload from each response in the stream + // might be of different types. This is to simulate a mixed type of payload + // stream. + PayloadType response_type = 1; + + // Configuration for each expected response message. + repeated ResponseParameters response_parameters = 2; + + // Optional input payload sent along with the request. + Payload payload = 3; + + // Whether server should return a given status + EchoStatus response_status = 7; +} + +// Server-streaming response, as configured by the request and parameters. +message StreamingOutputCallResponse { + // Payload to increase response size. + Payload payload = 1; +} + +// For reconnect interop test only. +// Client tells server what reconnection parameters it used. +message ReconnectParams { + int32 max_reconnect_backoff_ms = 1; +} + +// For reconnect interop test only. +// Server tells client whether its reconnects are following the spec and the +// reconnect backoffs it saw. +message ReconnectInfo { + bool passed = 1; + repeated int32 backoff_ms = 2; +} diff --git a/testassets/Proto/test.proto b/testassets/Proto/test.proto new file mode 100644 index 000000000..86d6ab605 --- /dev/null +++ b/testassets/Proto/test.proto @@ -0,0 +1,79 @@ + +// Copyright 2015-2016 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// An integration test service that covers all the method signature permutations +// of unary/streaming requests/responses. + +syntax = "proto3"; + +import "empty.proto"; +import "messages.proto"; + +package grpc.testing; + +// A simple service to test the various types of RPCs and experiment with +// performance with various types of payload. +service TestService { + // One empty request followed by one empty response. + rpc EmptyCall(grpc.testing.Empty) returns (grpc.testing.Empty); + + // One request followed by one response. + rpc UnaryCall(SimpleRequest) returns (SimpleResponse); + + // One request followed by one response. Response has cache control + // headers set such that a caching HTTP proxy (such as GFE) can + // satisfy subsequent requests. + rpc CacheableUnaryCall(SimpleRequest) returns (SimpleResponse); + + // One request followed by a sequence of responses (streamed download). + // The server returns the payload with client desired type and sizes. + rpc StreamingOutputCall(StreamingOutputCallRequest) + returns (stream StreamingOutputCallResponse); + + // A sequence of requests followed by one response (streamed upload). + // The server returns the aggregated size of client payload as the result. + rpc StreamingInputCall(stream StreamingInputCallRequest) + returns (StreamingInputCallResponse); + + // A sequence of requests with each request served by the server immediately. + // As one request could lead to multiple responses, this interface + // demonstrates the idea of full duplexing. + rpc FullDuplexCall(stream StreamingOutputCallRequest) + returns (stream StreamingOutputCallResponse); + + // A sequence of requests followed by a sequence of responses. + // The server buffers all the client requests and then serves them in order. A + // stream of responses are returned to the client when the server starts with + // first request. + rpc HalfDuplexCall(stream StreamingOutputCallRequest) + returns (stream StreamingOutputCallResponse); + + // The test server will not implement this method. It will be used + // to test the behavior when clients call unimplemented methods. + rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty); +} + +// A simple service NOT implemented at servers so clients can test for +// that case. +service UnimplementedService { + // A call that no server should implement + rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty); +} + +// A service used to control reconnect server. +service ReconnectService { + rpc Start(grpc.testing.ReconnectParams) returns (grpc.testing.Empty); + rpc Stop(grpc.testing.Empty) returns (grpc.testing.ReconnectInfo); +} From 547a77c1315d1548d338338630ce7cca6d624b60 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 26 Apr 2019 19:58:23 +1200 Subject: [PATCH 03/13] Call cancellation and deadline --- Directory.Build.props | 2 + examples/Clients/Counter/Counter.csproj | 1 - examples/Clients/Greeter/Greeter.csproj | 1 - examples/Clients/Mailer/Mailer.csproj | 1 - examples/Clients/Reflector/Reflector.csproj | 1 - .../BenchmarkClient/BenchmarkClient.csproj | 1 - .../BenchmarkServer/BenchmarkServer.csproj | 1 - .../Internal/GrpcCall.cs | 88 +++++++++++++++---- .../Internal/HttpContentClientStreamWriter.cs | 17 +++- .../Internal/HttpContextClientStreamReader.cs | 64 ++++++++------ test/FunctionalTests/NestedTests.cs | 1 + .../Grpc.AspNetCore.Server.Tests.csproj | 1 - .../CancellationTests.cs | 71 +++++++++++++++ .../DeadlineTests.cs | 84 ++++++++++++++++++ .../GetTrailersTests.cs | 2 +- .../Grpc.NetCore.HttpClient.Tests.csproj | 1 - .../Infrastructure/ResponseUtils.cs | 7 +- .../ResponseAsyncTests.cs | 3 +- .../InteropTestsClient/InteropClient.cs | 2 +- testassets/InteropTestsClient/RunTests.ps1 | 4 +- 20 files changed, 294 insertions(+), 59 deletions(-) create mode 100644 test/Grpc.NetCore.HttpClient.Tests/CancellationTests.cs diff --git a/Directory.Build.props b/Directory.Build.props index b8e6f2fcd..8dbdf8c3c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,6 +9,8 @@ $(WarningsNotAsErrors);CS1591 + + preview diff --git a/examples/Clients/Counter/Counter.csproj b/examples/Clients/Counter/Counter.csproj index 838b2a396..10d1873c5 100644 --- a/examples/Clients/Counter/Counter.csproj +++ b/examples/Clients/Counter/Counter.csproj @@ -3,7 +3,6 @@ Exe netcoreapp3.0 - latest diff --git a/examples/Clients/Greeter/Greeter.csproj b/examples/Clients/Greeter/Greeter.csproj index fe67b0566..c11bb2333 100644 --- a/examples/Clients/Greeter/Greeter.csproj +++ b/examples/Clients/Greeter/Greeter.csproj @@ -3,7 +3,6 @@ Exe netcoreapp3.0 - latest diff --git a/examples/Clients/Mailer/Mailer.csproj b/examples/Clients/Mailer/Mailer.csproj index 4f96ecea4..621e8dc73 100644 --- a/examples/Clients/Mailer/Mailer.csproj +++ b/examples/Clients/Mailer/Mailer.csproj @@ -3,7 +3,6 @@ Exe netcoreapp3.0 - latest diff --git a/examples/Clients/Reflector/Reflector.csproj b/examples/Clients/Reflector/Reflector.csproj index ea7110ca9..d23ab6d4b 100644 --- a/examples/Clients/Reflector/Reflector.csproj +++ b/examples/Clients/Reflector/Reflector.csproj @@ -3,7 +3,6 @@ Exe netcoreapp3.0 - latest diff --git a/perf/benchmarkapps/BenchmarkClient/BenchmarkClient.csproj b/perf/benchmarkapps/BenchmarkClient/BenchmarkClient.csproj index 5ed86e53e..494916146 100644 --- a/perf/benchmarkapps/BenchmarkClient/BenchmarkClient.csproj +++ b/perf/benchmarkapps/BenchmarkClient/BenchmarkClient.csproj @@ -3,7 +3,6 @@ Exe netcoreapp3.0 - latest diff --git a/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj b/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj index 810045d5b..4ab4e4c23 100644 --- a/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj +++ b/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj @@ -4,7 +4,6 @@ netcoreapp3.0 $(BenchmarksTargetFramework) Exe - latest InProcess diff --git a/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs index bedececf1..3b9e227fa 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs @@ -30,11 +30,16 @@ namespace Grpc.NetCore.HttpClient.Internal internal class GrpcCall { private readonly CancellationTokenSource _callCts; + private readonly CancellationTokenRegistration? _ctsRegistration; private readonly ISystemClock _clock; + private readonly TimeSpan? _timeout; + private readonly Timer _deadlineTimer; private HttpResponseMessage _httpResponse; private Metadata _trailers; + private CancellationTokenRegistration? _writerCtsRegistration; + public bool DeadlineReached { get; private set; } public bool Disposed { get; private set; } public bool ResponseFinished { get; private set; } public CallOptions Options { get; } @@ -50,6 +55,36 @@ public GrpcCall(Method method, CallOptions options, ISystem Method = method; Options = options; _clock = clock; + + if (options.CancellationToken.CanBeCanceled) + { + _ctsRegistration = options.CancellationToken.Register(CancelCall); + } + + if (options.Deadline != null && options.Deadline != DateTime.MaxValue) + { + var timeout = options.Deadline.Value - _clock.UtcNow; + _timeout = (timeout > TimeSpan.Zero) ? timeout : TimeSpan.Zero; + } + + if (_timeout != null) + { + _deadlineTimer = new Timer(ReachDeadline, null, _timeout.Value, Timeout.InfiniteTimeSpan); + } + } + + private void ReachDeadline(object state) + { + if (!_callCts.IsCancellationRequested) + { + DeadlineReached = true; + _callCts.Cancel(); + } + } + + private void CancelCall() + { + _callCts.Cancel(); } public CancellationToken CancellationToken @@ -109,6 +144,9 @@ public void Dispose() _callCts.Cancel(); _callCts.Dispose(); + _ctsRegistration?.Dispose(); + _writerCtsRegistration?.Dispose(); + _deadlineTimer?.Dispose(); _httpResponse?.Dispose(); StreamReader?.Dispose(); ClientStreamWriter?.Dispose(); @@ -124,6 +162,11 @@ private HttpContentClientStreamWriter CreateWriter(HttpRequ { TaskCompletionSource writeStreamTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); TaskCompletionSource completeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _writerCtsRegistration = _callCts.Token.Register(() => + { + completeTcs.TrySetCanceled(); + writeStreamTcs.TrySetCanceled(); + }); message.Content = new PushStreamContent( (stream) => @@ -157,12 +200,10 @@ private HttpRequestMessage CreateHttpRequestMessage() } } - if (Options.Deadline != null && Options.Deadline != DateTime.MaxValue) + if (_timeout != null) { - var deadline = Options.Deadline.Value - _clock.UtcNow; - // JamesNK(todo) - Replicate C core's logic for formatting grpc-timeout - message.Headers.Add(GrpcProtocolConstants.TimeoutHeader, Convert.ToInt64(deadline.TotalMilliseconds) + "m"); + message.Headers.Add(GrpcProtocolConstants.TimeoutHeader, Convert.ToInt64(_timeout.Value.TotalMilliseconds) + "m"); } return message; @@ -178,21 +219,38 @@ public void EnsureNotDisposed() public async Task GetResponseAsync() { - _httpResponse = await SendTask.ConfigureAwait(false); - - // Server might have returned a status without any response body. For example, an unimplemented status - // Check for the trailer status before attempting to read the body and failing - if (_httpResponse.TrailingHeaders.Contains(GrpcProtocolConstants.StatusTrailer)) + try { + _httpResponse = await SendTask.ConfigureAwait(false); + + // Server might have returned a status without any response body. For example, an unimplemented status + // Check for the trailer status before attempting to read the body and failing + if (_httpResponse.TrailingHeaders.Contains(GrpcProtocolConstants.StatusTrailer)) + { + FinishResponse(_httpResponse); + } + + var responseStream = await _httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); + var message = await responseStream.ReadSingleMessageAsync(Method.ResponseMarshaller.Deserializer, _callCts.Token).ConfigureAwait(false); FinishResponse(_httpResponse); - } - var responseStream = await _httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); - var message = await responseStream.ReadSingleMessageAsync(Method.ResponseMarshaller.Deserializer, _callCts.Token).ConfigureAwait(false); - FinishResponse(_httpResponse); + // The task of this method is cached so there is no need to cache the message here + return message; + } + catch (TaskCanceledException) + { + throw CreateCanceledStatusException(); + } + catch (OperationCanceledException) + { + throw CreateCanceledStatusException(); + } + } - // The task of this method is cached so there is no need to cache the message here - return message; + internal RpcException CreateCanceledStatusException() + { + var statusCode = DeadlineReached ? StatusCode.DeadlineExceeded : StatusCode.Cancelled; + return new RpcException(new Status(statusCode, string.Empty)); } internal void FinishResponse(HttpResponseMessage httpResponseMessage) diff --git a/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamWriter.cs b/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamWriter.cs index 4c954e97e..0cd8cedb9 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamWriter.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamWriter.cs @@ -74,6 +74,10 @@ public Task WriteAsync(TRequest message) { return Task.FromException(new InvalidOperationException("Cannot write message because the client stream writer is complete.")); } + else if (_completeTcs.Task.IsCanceled) + { + throw _call.CreateCanceledStatusException(); + } // Pending writes need to be awaited first if (IsWriteInProgress) @@ -94,10 +98,17 @@ public void Dispose() private async Task WriteAsyncCore(TRequest message) { - // Wait until the client stream has started - var writeStream = await _writeStreamTask.ConfigureAwait(false); + try + { + // Wait until the client stream has started + var writeStream = await _writeStreamTask.ConfigureAwait(false); - await SerialiationHelpers.WriteMessage(writeStream, message, _call.Method.RequestMarshaller.Serializer, _call.CancellationToken).ConfigureAwait(false); + await SerialiationHelpers.WriteMessage(writeStream, message, _call.Method.RequestMarshaller.Serializer, _call.CancellationToken).ConfigureAwait(false); + } + catch (TaskCanceledException) + { + throw _call.CreateCanceledStatusException(); + } } private bool IsWriteInProgress diff --git a/src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs b/src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs index 14f8912e8..18682735c 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs @@ -53,40 +53,52 @@ public async Task MoveNext(CancellationToken cancellationToken) // User could have disposed call _call.EnsureNotDisposed(); - // Linking tokens is expensive. Only create a linked token is passed in cancellation token requires it - CancellationTokenSource cts = null; - if (cancellationToken.CanBeCanceled) + if (_call.CancellationToken.IsCancellationRequested) { - cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _call.CancellationToken); - cancellationToken = cts.Token; + throw _call.CreateCanceledStatusException(); } - else - { - cancellationToken = _call.CancellationToken; - } - - cancellationToken.ThrowIfCancellationRequested(); - if (_httpResponse == null) - { - _httpResponse = await _call.SendTask.ConfigureAwait(false); - } - if (_responseStream == null) + try { - _responseStream = await _httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); - } + // Linking tokens is expensive. Only create a linked token is passed in cancellation token requires it + CancellationTokenSource cts = null; + if (cancellationToken.CanBeCanceled) + { + cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _call.CancellationToken); + cancellationToken = cts.Token; + } + else + { + cancellationToken = _call.CancellationToken; + } - using (cts) - { - Current = await _responseStream.ReadStreamedMessageAsync(_call.Method.ResponseMarshaller.Deserializer, cancellationToken).ConfigureAwait(false); - if (Current == null) + cancellationToken.ThrowIfCancellationRequested(); + + if (_httpResponse == null) { - // No more content in response so mark as finished - _call.FinishResponse(_httpResponse); - return false; + _httpResponse = await _call.SendTask.ConfigureAwait(false); + } + if (_responseStream == null) + { + _responseStream = await _httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); } - return true; + using (cts) + { + Current = await _responseStream.ReadStreamedMessageAsync(_call.Method.ResponseMarshaller.Deserializer, cancellationToken).ConfigureAwait(false); + if (Current == null) + { + // No more content in response so mark as finished + _call.FinishResponse(_httpResponse); + return false; + } + + return true; + } + } + catch (TaskCanceledException) + { + throw _call.CreateCanceledStatusException(); } } } diff --git a/test/FunctionalTests/NestedTests.cs b/test/FunctionalTests/NestedTests.cs index 750a9b4fd..0c69462e8 100644 --- a/test/FunctionalTests/NestedTests.cs +++ b/test/FunctionalTests/NestedTests.cs @@ -35,6 +35,7 @@ namespace Grpc.AspNetCore.FunctionalTests public class NestedTests : FunctionalTestBase { [Test] + [Ignore("Failing because TestHost does not return trailers. Blocked on https://github.com/aspnet/AspNetCore/issues/6880")] public async Task CallNestedService_SuccessResponse() { // Arrange diff --git a/test/Grpc.AspNetCore.Server.Tests/Grpc.AspNetCore.Server.Tests.csproj b/test/Grpc.AspNetCore.Server.Tests/Grpc.AspNetCore.Server.Tests.csproj index 898b201b3..2e66bacdd 100644 --- a/test/Grpc.AspNetCore.Server.Tests/Grpc.AspNetCore.Server.Tests.csproj +++ b/test/Grpc.AspNetCore.Server.Tests/Grpc.AspNetCore.Server.Tests.csproj @@ -3,7 +3,6 @@ netcoreapp3.0 false - latest diff --git a/test/Grpc.NetCore.HttpClient.Tests/CancellationTests.cs b/test/Grpc.NetCore.HttpClient.Tests/CancellationTests.cs new file mode 100644 index 000000000..dcd780f17 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/CancellationTests.cs @@ -0,0 +1,71 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using NUnit.Framework; +using Greet; +using static Greet.Greeter; +using Grpc.Core; +using Google.Protobuf; +using System.Net.Http; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using System.Net; +using System.Threading.Tasks; +using System; +using System.IO; +using System.Threading; +using System.Net.Http.Headers; +using System.Text; +using System.Linq; +using Grpc.NetCore.HttpClient.Internal; +using Grpc.Tests; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class CancellationTests + { + [Test] + public void AsyncClientStreamingCall_CancellationDuringSend_ResponseThrowsCancelledStatus() + { + // Arrange + var cts = new CancellationTokenSource(); + PushStreamContent content = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + content = (PushStreamContent)request.Content; + await content.PushComplete.DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(cancellationToken: cts.Token)); + + // Assert + var responseTask = call.ResponseAsync; + Assert.IsFalse(responseTask.IsCompleted, "Response not returned until client stream is complete."); + + cts.Cancel(); + + var ex = Assert.ThrowsAsync(async () => await responseTask.DefaultTimeout()); + Assert.AreEqual(StatusCode.Cancelled, ex.Status.StatusCode); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs b/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs index 48b6c0c60..67f802e99 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs @@ -120,6 +120,90 @@ public async Task AsyncUnaryCall_SendDeadlineHeaderAndDeadlineValue_DeadlineValu Assert.AreEqual("1000m", httpRequestMessage.Headers.GetValues("grpc-timeout").Single()); } + [Test] + public void AsyncClientStreamingCall_DeadlineDuringSend_ResponseThrowsDeadlineExceededStatus() + { + // Arrange + PushStreamContent content = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + content = (PushStreamContent)request.Content; + await content.PushComplete.DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: DateTime.UtcNow.AddSeconds(0.5))); + + // Assert + var responseTask = call.ResponseAsync; + Assert.IsFalse(responseTask.IsCompleted, "Response not returned until client stream is complete."); + + var ex = Assert.ThrowsAsync(async () => await responseTask.DefaultTimeout()); + Assert.AreEqual(StatusCode.DeadlineExceeded, ex.Status.StatusCode); + } + + [Test] + public void AsyncClientStreamingCall_DeadlineBeforeWrite_ResponseThrowsDeadlineExceededStatus() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK)); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: DateTime.UtcNow)); + + // Assert + var ex = Assert.ThrowsAsync(async () => await call.RequestStream.WriteAsync(new HelloRequest()).DefaultTimeout()); + Assert.AreEqual(StatusCode.DeadlineExceeded, ex.Status.StatusCode); + } + + [Test] + public void AsyncClientStreamingCall_DeadlineDuringWrite_ResponseThrowsDeadlineExceededStatus() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK)); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: DateTime.UtcNow.AddSeconds(0.5))); + + // Assert + var ex = Assert.ThrowsAsync(async () => await call.RequestStream.WriteAsync(new HelloRequest()).DefaultTimeout()); + Assert.AreEqual(StatusCode.DeadlineExceeded, ex.Status.StatusCode); + } + + [Test] + public void AsyncServerStreamingCall_DeadlineDuringWrite_ResponseThrowsDeadlineExceededStatus() + { + // Arrange + var cts = new CancellationTokenSource(); + + var httpClient = TestHelpers.CreateTestClient(request => + { + var stream = new SyncPointMemoryStream(); + var content = new StreamContent(stream); + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, content)); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: DateTime.UtcNow.AddSeconds(0.5)), new HelloRequest()); + + // Assert + var ex = Assert.ThrowsAsync(async () => await call.ResponseStream.MoveNext(CancellationToken.None)); + Assert.AreEqual(StatusCode.DeadlineExceeded, ex.Status.StatusCode); + } + private class TestSystemClock : ISystemClock { public TestSystemClock(DateTime utcNow) diff --git a/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs b/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs index d378f51eb..0ed27f960 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs @@ -285,7 +285,7 @@ public void AsyncClientStreamingCall_UncompleteWriter_ThrowsError() { var stream = new SyncPointMemoryStream(); - var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, new StreamContent(stream)); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, new StreamContent(stream), grpcStatusCode: null); return Task.FromResult(response); }); var invoker = new HttpClientCallInvoker(httpClient); diff --git a/test/Grpc.NetCore.HttpClient.Tests/Grpc.NetCore.HttpClient.Tests.csproj b/test/Grpc.NetCore.HttpClient.Tests/Grpc.NetCore.HttpClient.Tests.csproj index 0c7ddd570..c2dfb4bba 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/Grpc.NetCore.HttpClient.Tests.csproj +++ b/test/Grpc.NetCore.HttpClient.Tests/Grpc.NetCore.HttpClient.Tests.csproj @@ -3,7 +3,6 @@ netcoreapp3.0 false - latest diff --git a/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/ResponseUtils.cs b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/ResponseUtils.cs index e28e94cd4..be807256b 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/ResponseUtils.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/ResponseUtils.cs @@ -40,14 +40,17 @@ public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, stri public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, byte[] payload) => CreateResponse(statusCode, new ByteArrayContent(payload)); - public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, HttpContent payload, StatusCode grpcStatusCode = StatusCode.OK) + public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, HttpContent payload, StatusCode? grpcStatusCode = StatusCode.OK) { var message = new HttpResponseMessage(statusCode) { Content = payload }; - message.TrailingHeaders.Add(GrpcProtocolConstants.StatusTrailer, grpcStatusCode.ToString("D")); + if (grpcStatusCode != null) + { + message.TrailingHeaders.Add(GrpcProtocolConstants.StatusTrailer, grpcStatusCode.Value.ToString("D")); + } return message; } diff --git a/test/Grpc.NetCore.HttpClient.Tests/ResponseAsyncTests.cs b/test/Grpc.NetCore.HttpClient.Tests/ResponseAsyncTests.cs index 94213ccaa..edab5b484 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/ResponseAsyncTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/ResponseAsyncTests.cs @@ -85,7 +85,8 @@ public async Task AsyncUnaryCall_DisposeAfterHeadersAndBeforeMessage_ThrowsError call.Dispose(); // Assert - Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout()); + var ex = Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout()); + Assert.AreEqual(StatusCode.Cancelled, ex.StatusCode); var header = responseHeaders.Single(h => h.Key == "custom"); Assert.AreEqual("value!", header.Value); diff --git a/testassets/InteropTestsClient/InteropClient.cs b/testassets/InteropTestsClient/InteropClient.cs index b8e5b959c..42b73ead2 100644 --- a/testassets/InteropTestsClient/InteropClient.cs +++ b/testassets/InteropTestsClient/InteropClient.cs @@ -55,7 +55,7 @@ private class ClientOptions [Option("server_port", Default = 50052)] public int ServerPort { get; set; } - [Option("test_case", Default = "unimplemented_method")] + [Option("test_case", Default = "timeout_on_sleeping_server")] public string TestCase { get; set; } // Deliberately using nullable bool type to allow --use_tls=true syntax (as opposed to --use_tls) diff --git a/testassets/InteropTestsClient/RunTests.ps1 b/testassets/InteropTestsClient/RunTests.ps1 index f0256ce75..c2093dced 100644 --- a/testassets/InteropTestsClient/RunTests.ps1 +++ b/testassets/InteropTestsClient/RunTests.ps1 @@ -11,9 +11,9 @@ #"oauth2_auth_token", #"per_rpc_creds", - #"cancel_after_begin", + "cancel_after_begin", #"cancel_after_first_response", - #"timeout_on_sleeping_server", + "timeout_on_sleeping_server", "custom_metadata", "status_code_and_message", "unimplemented_service", From b3c37287ec4ca34ac55be2fbad9c6f7b28793e2e Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 26 Apr 2019 20:42:03 +1200 Subject: [PATCH 04/13] PR feedback --- Directory.Build.props | 1 + build/dependencies.props | 3 +- .../Grpc.NetCore.HttpClient.csproj | 3 +- .../HttpClientCallInvoker.cs | 4 +- .../Internal/GrpcCall.cs | 24 +++-- .../Internal/HttpContentClientStreamWriter.cs | 6 +- .../Internal/HttpContextClientStreamReader.cs | 4 +- .../Internal/SerialiationHelpers.cs | 2 +- test/FunctionalTests/AuthorizationTests.cs | 2 +- .../ClientStreamingMethodTests.cs | 2 +- test/FunctionalTests/CompressionTests.cs | 2 +- test/FunctionalTests/DeadlineTests.cs | 2 +- .../DuplexStreamingMethodTests.cs | 2 +- test/FunctionalTests/HttpContextTests.cs | 2 +- .../HttpResponseMessageExtensions.cs | 2 +- test/FunctionalTests/LifetimeTests.cs | 2 +- test/FunctionalTests/MaxMessageSizeTests.cs | 2 +- test/FunctionalTests/NestedTests.cs | 2 +- .../ServerStreamingMethodTests.cs | 2 +- test/FunctionalTests/UnaryMethodTests.cs | 2 +- test/FunctionalTests/UnimplementedTests.cs | 2 +- .../HttpContextStreamReaderTests.cs | 2 +- .../HttpContextStreamWriterTests.cs | 1 + .../PipeExtensionsTests.cs | 1 + .../ReflectionGrpcServiceActivatorTests.cs | 1 + .../AsyncClientStreamingCallTests.cs | 2 +- .../AsyncDuplexStreamingCallTests.cs | 5 +- .../AsyncServerStreamingCallTests.cs | 5 +- .../AsyncUnaryCallTests.cs | 2 +- .../CancellationTests.cs | 2 +- .../DeadlineTests.cs | 4 +- .../GetTrailersTests.cs | 2 +- .../HeadersTests.cs | 2 +- .../HttpContextClientStreamReaderTests.cs | 89 +++++++++++++++++++ .../ResponseAsyncTests.cs | 2 +- .../ResponseHeadersAsyncTests.cs | 2 +- .../HttpContextServerCallContextHelpers.cs | 2 +- test/Shared/MessageHelpers.cs | 2 +- test/Shared/SyncPoint.cs | 2 +- test/Shared/SyncPointMemoryStream.cs | 2 +- test/Shared/TaskExtensions.cs | 2 +- 41 files changed, 152 insertions(+), 53 deletions(-) create mode 100644 test/Grpc.NetCore.HttpClient.Tests/HttpContextClientStreamReaderTests.cs diff --git a/Directory.Build.props b/Directory.Build.props index 8dbdf8c3c..c536a913b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,6 +10,7 @@ $(WarningsNotAsErrors);CS1591 + preview diff --git a/build/dependencies.props b/build/dependencies.props index edae8b3f7..090dc3daa 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -1,6 +1,7 @@ 0.11.3 + 3.0.0 3.7.0 1.20.0-pre3 1.20.0-pre3 @@ -12,7 +13,7 @@ 16.0.0-preview-20181205-02 1.0.0-beta2-18618-05 4.10.0 - 12.0.1 + 12.0.2 3.11.0 3.12.0 4.6.0-preview.19073.11 diff --git a/src/Grpc.NetCore.HttpClient/Grpc.NetCore.HttpClient.csproj b/src/Grpc.NetCore.HttpClient/Grpc.NetCore.HttpClient.csproj index 2b0c69f4d..57a36a628 100644 --- a/src/Grpc.NetCore.HttpClient/Grpc.NetCore.HttpClient.csproj +++ b/src/Grpc.NetCore.HttpClient/Grpc.NetCore.HttpClient.csproj @@ -12,6 +12,7 @@ + netcoreapp3.0 8.0 true @@ -20,7 +21,7 @@ - + diff --git a/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs b/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs index 2c6f7a765..96ae3afe6 100644 --- a/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs +++ b/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs @@ -97,7 +97,7 @@ public override AsyncDuplexStreamingCall AsyncDuplexStreami return new AsyncDuplexStreamingCall( requestStream: call.ClientStreamWriter, - responseStream: call.StreamReader, + responseStream: call.ClientStreamReader, responseHeadersAsync: call.GetResponseHeadersAsync(), getStatusFunc: call.GetStatus, getTrailersFunc: call.GetTrailers, @@ -114,7 +114,7 @@ public override AsyncServerStreamingCall AsyncServerStreamingCall( - responseStream: call.StreamReader, + responseStream: call.ClientStreamReader, responseHeadersAsync: call.GetResponseHeadersAsync(), getStatusFunc: call.GetStatus, getTrailersFunc: call.GetTrailers, diff --git a/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs index 3b9e227fa..1fb83f74f 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs @@ -46,8 +46,7 @@ internal class GrpcCall public Method Method { get; } public Task SendTask { get; private set; } public HttpContentClientStreamWriter ClientStreamWriter { get; private set; } - - public HttpContextClientStreamReader StreamReader { get; private set; } + public HttpContextClientStreamReader ClientStreamReader { get; private set; } public GrpcCall(Method method, CallOptions options, ISystemClock clock) { @@ -94,7 +93,7 @@ public CancellationToken CancellationToken public void SendUnary(System.Net.Http.HttpClient client, TRequest request) { - HttpRequestMessage message = CreateHttpRequestMessage(); + var message = CreateHttpRequestMessage(); SetMessageContent(request, message); SendCore(client, message); } @@ -104,7 +103,7 @@ private void SetMessageContent(TRequest request, HttpRequestMessage message) message.Content = new PushStreamContent( (stream) => { - return SerialiationHelpers.WriteMessage(stream, request, Method.RequestMarshaller.Serializer, Options.CancellationToken); + return SerializationHelpers.WriteMessage(stream, request, Method.RequestMarshaller.Serializer, Options.CancellationToken); }, GrpcProtocolConstants.GrpcContentTypeHeaderValue); } @@ -119,11 +118,11 @@ public void SendClientStreaming(System.Net.Http.HttpClient client) public void SendServerStreaming(System.Net.Http.HttpClient client, TRequest request) { - HttpRequestMessage message = CreateHttpRequestMessage(); + var message = CreateHttpRequestMessage(); SetMessageContent(request, message); SendCore(client, message); - StreamReader = new HttpContextClientStreamReader(this); + ClientStreamReader = new HttpContextClientStreamReader(this); } public void SendDuplexStreaming(System.Net.Http.HttpClient client) @@ -133,7 +132,7 @@ public void SendDuplexStreaming(System.Net.Http.HttpClient client) SendCore(client, message); - StreamReader = new HttpContextClientStreamReader(this); + ClientStreamReader = new HttpContextClientStreamReader(this); } public void Dispose() @@ -148,7 +147,7 @@ public void Dispose() _writerCtsRegistration?.Dispose(); _deadlineTimer?.Dispose(); _httpResponse?.Dispose(); - StreamReader?.Dispose(); + ClientStreamReader?.Dispose(); ClientStreamWriter?.Dispose(); } } @@ -160,8 +159,8 @@ private void SendCore(System.Net.Http.HttpClient client, HttpRequestMessage mess private HttpContentClientStreamWriter CreateWriter(HttpRequestMessage message) { - TaskCompletionSource writeStreamTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - TaskCompletionSource completeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var writeStreamTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var completeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _writerCtsRegistration = _callCts.Token.Register(() => { completeTcs.TrySetCanceled(); @@ -237,10 +236,6 @@ public async Task GetResponseAsync() // The task of this method is cached so there is no need to cache the message here return message; } - catch (TaskCanceledException) - { - throw CreateCanceledStatusException(); - } catch (OperationCanceledException) { throw CreateCanceledStatusException(); @@ -304,6 +299,7 @@ private static Status GetStatusCore(HttpResponseMessage httpResponseMessage) string grpcMessage = null; if (httpResponseMessage.TrailingHeaders.TryGetValues(GrpcProtocolConstants.MessageTrailer, out var grpcMessageValues)) { + // TODO(JamesNK): Unescape percent encoding grpcMessage = grpcMessageValues.FirstOrDefault(); } diff --git a/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamWriter.cs b/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamWriter.cs index 0cd8cedb9..20e3c8f7b 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamWriter.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamWriter.cs @@ -103,7 +103,7 @@ private async Task WriteAsyncCore(TRequest message) // Wait until the client stream has started var writeStream = await _writeStreamTask.ConfigureAwait(false); - await SerialiationHelpers.WriteMessage(writeStream, message, _call.Method.RequestMarshaller.Serializer, _call.CancellationToken).ConfigureAwait(false); + await SerializationHelpers.WriteMessage(writeStream, message, _call.Method.RequestMarshaller.Serializer, _call.CancellationToken).ConfigureAwait(false); } catch (TaskCanceledException) { @@ -111,6 +111,10 @@ private async Task WriteAsyncCore(TRequest message) } } + /// + /// A value indicating whether there is an async write already in progress. + /// Should only check this property when holding the write lock. + /// private bool IsWriteInProgress { get diff --git a/src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs b/src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs index 18682735c..3976cd5c2 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs @@ -60,7 +60,7 @@ public async Task MoveNext(CancellationToken cancellationToken) try { - // Linking tokens is expensive. Only create a linked token is passed in cancellation token requires it + // Linking tokens is expensive. Only create a linked token if the token passed in requires it CancellationTokenSource cts = null; if (cancellationToken.CanBeCanceled) { @@ -96,7 +96,7 @@ public async Task MoveNext(CancellationToken cancellationToken) return true; } } - catch (TaskCanceledException) + catch (OperationCanceledException) { throw _call.CreateCanceledStatusException(); } diff --git a/src/Grpc.NetCore.HttpClient/Internal/SerialiationHelpers.cs b/src/Grpc.NetCore.HttpClient/Internal/SerialiationHelpers.cs index 2c7826cde..3715cbc54 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/SerialiationHelpers.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/SerialiationHelpers.cs @@ -25,7 +25,7 @@ namespace Grpc.NetCore.HttpClient.Internal { - internal static class SerialiationHelpers + internal static class SerializationHelpers { public static async Task WriteMessage(Stream stream, TMessage message, Func serializer, CancellationToken cancellationToken) { diff --git a/test/FunctionalTests/AuthorizationTests.cs b/test/FunctionalTests/AuthorizationTests.cs index d279ec1d6..f44cc3cde 100644 --- a/test/FunctionalTests/AuthorizationTests.cs +++ b/test/FunctionalTests/AuthorizationTests.cs @@ -25,7 +25,7 @@ using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; using Grpc.Core; -using Grpc.Tests; +using Grpc.Tests.Shared; using NUnit.Framework; namespace Grpc.AspNetCore.FunctionalTests diff --git a/test/FunctionalTests/ClientStreamingMethodTests.cs b/test/FunctionalTests/ClientStreamingMethodTests.cs index efc998188..053220cc3 100644 --- a/test/FunctionalTests/ClientStreamingMethodTests.cs +++ b/test/FunctionalTests/ClientStreamingMethodTests.cs @@ -27,9 +27,9 @@ using Google.Protobuf.WellKnownTypes; using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; -using Grpc.Tests; using Grpc.Core; using NUnit.Framework; +using Grpc.Tests.Shared; namespace Grpc.AspNetCore.FunctionalTests { diff --git a/test/FunctionalTests/CompressionTests.cs b/test/FunctionalTests/CompressionTests.cs index ec5d51924..16c802d21 100644 --- a/test/FunctionalTests/CompressionTests.cs +++ b/test/FunctionalTests/CompressionTests.cs @@ -29,7 +29,7 @@ using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Compression; using Grpc.AspNetCore.Server.Internal; -using Grpc.Tests; +using Grpc.Tests.Shared; using Grpc.Core; using NUnit.Framework; diff --git a/test/FunctionalTests/DeadlineTests.cs b/test/FunctionalTests/DeadlineTests.cs index 7eb8f4c96..6e6edbc61 100644 --- a/test/FunctionalTests/DeadlineTests.cs +++ b/test/FunctionalTests/DeadlineTests.cs @@ -26,7 +26,7 @@ using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; using Grpc.Core; -using Grpc.Tests; +using Grpc.Tests.Shared; using NUnit.Framework; namespace Grpc.AspNetCore.FunctionalTests diff --git a/test/FunctionalTests/DuplexStreamingMethodTests.cs b/test/FunctionalTests/DuplexStreamingMethodTests.cs index 4268cced0..76a666cac 100644 --- a/test/FunctionalTests/DuplexStreamingMethodTests.cs +++ b/test/FunctionalTests/DuplexStreamingMethodTests.cs @@ -27,7 +27,7 @@ using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; using Grpc.Core; -using Grpc.Tests; +using Grpc.Tests.Shared; using NUnit.Framework; namespace Grpc.AspNetCore.FunctionalTests diff --git a/test/FunctionalTests/HttpContextTests.cs b/test/FunctionalTests/HttpContextTests.cs index 4ccfd024b..331fe1aaa 100644 --- a/test/FunctionalTests/HttpContextTests.cs +++ b/test/FunctionalTests/HttpContextTests.cs @@ -22,7 +22,7 @@ using Greet; using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; -using Grpc.Tests; +using Grpc.Tests.Shared; using Grpc.Core; using NUnit.Framework; diff --git a/test/FunctionalTests/Infrastructure/HttpResponseMessageExtensions.cs b/test/FunctionalTests/Infrastructure/HttpResponseMessageExtensions.cs index 8b02cef79..7dad7ed19 100644 --- a/test/FunctionalTests/Infrastructure/HttpResponseMessageExtensions.cs +++ b/test/FunctionalTests/Infrastructure/HttpResponseMessageExtensions.cs @@ -20,7 +20,7 @@ using System.Net.Http; using System.Threading.Tasks; using Google.Protobuf; -using Grpc.Tests; +using Grpc.Tests.Shared; using NUnit.Framework; namespace Grpc.AspNetCore.FunctionalTests.Infrastructure diff --git a/test/FunctionalTests/LifetimeTests.cs b/test/FunctionalTests/LifetimeTests.cs index 768fa76e7..339db2a7a 100644 --- a/test/FunctionalTests/LifetimeTests.cs +++ b/test/FunctionalTests/LifetimeTests.cs @@ -25,7 +25,7 @@ using Google.Protobuf.WellKnownTypes; using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; -using Grpc.Tests; +using Grpc.Tests.Shared; using Grpc.Core; using Lifetime; using NUnit.Framework; diff --git a/test/FunctionalTests/MaxMessageSizeTests.cs b/test/FunctionalTests/MaxMessageSizeTests.cs index a3253f94f..ad53ff799 100644 --- a/test/FunctionalTests/MaxMessageSizeTests.cs +++ b/test/FunctionalTests/MaxMessageSizeTests.cs @@ -23,7 +23,7 @@ using Greet; using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; -using Grpc.Tests; +using Grpc.Tests.Shared; using Grpc.Core; using NUnit.Framework; diff --git a/test/FunctionalTests/NestedTests.cs b/test/FunctionalTests/NestedTests.cs index 0c69462e8..dafa43abe 100644 --- a/test/FunctionalTests/NestedTests.cs +++ b/test/FunctionalTests/NestedTests.cs @@ -24,7 +24,7 @@ using System.Threading.Tasks; using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; -using Grpc.Tests; +using Grpc.Tests.Shared; using Grpc.Core; using Nested; using NUnit.Framework; diff --git a/test/FunctionalTests/ServerStreamingMethodTests.cs b/test/FunctionalTests/ServerStreamingMethodTests.cs index 30b554e6b..f9cad3381 100644 --- a/test/FunctionalTests/ServerStreamingMethodTests.cs +++ b/test/FunctionalTests/ServerStreamingMethodTests.cs @@ -26,7 +26,7 @@ using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; using Grpc.Core; -using Grpc.Tests; +using Grpc.Tests.Shared; using NUnit.Framework; namespace Grpc.AspNetCore.FunctionalTests diff --git a/test/FunctionalTests/UnaryMethodTests.cs b/test/FunctionalTests/UnaryMethodTests.cs index 9011b0f1b..f10e81ebe 100644 --- a/test/FunctionalTests/UnaryMethodTests.cs +++ b/test/FunctionalTests/UnaryMethodTests.cs @@ -29,7 +29,7 @@ using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; using Grpc.Core; -using Grpc.Tests; +using Grpc.Tests.Shared; using NUnit.Framework; namespace Grpc.AspNetCore.FunctionalTests diff --git a/test/FunctionalTests/UnimplementedTests.cs b/test/FunctionalTests/UnimplementedTests.cs index 85a626b10..e9eb31579 100644 --- a/test/FunctionalTests/UnimplementedTests.cs +++ b/test/FunctionalTests/UnimplementedTests.cs @@ -25,7 +25,7 @@ using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; using Grpc.Core; -using Grpc.Tests; +using Grpc.Tests.Shared; using NUnit.Framework; namespace Grpc.AspNetCore.FunctionalTests diff --git a/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamReaderTests.cs b/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamReaderTests.cs index a5df11d86..9c00f7849 100644 --- a/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamReaderTests.cs +++ b/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamReaderTests.cs @@ -23,7 +23,7 @@ using Greet; using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; -using Grpc.Tests; +using Grpc.Tests.Shared; using Microsoft.AspNetCore.Http; using NUnit.Framework; diff --git a/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamWriterTests.cs b/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamWriterTests.cs index 91360240c..cfb54575d 100644 --- a/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamWriterTests.cs +++ b/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamWriterTests.cs @@ -24,6 +24,7 @@ using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; using Grpc.Core; +using Grpc.Tests.Shared; using Microsoft.AspNetCore.Http; using NUnit.Framework; diff --git a/test/Grpc.AspNetCore.Server.Tests/PipeExtensionsTests.cs b/test/Grpc.AspNetCore.Server.Tests/PipeExtensionsTests.cs index 26b71e420..32be8bd8a 100644 --- a/test/Grpc.AspNetCore.Server.Tests/PipeExtensionsTests.cs +++ b/test/Grpc.AspNetCore.Server.Tests/PipeExtensionsTests.cs @@ -27,6 +27,7 @@ using Grpc.AspNetCore.Server.Compression; using Grpc.AspNetCore.Server.Internal; using Grpc.Core; +using Grpc.Tests.Shared; using Microsoft.AspNetCore.Http; using NUnit.Framework; diff --git a/test/Grpc.AspNetCore.Server.Tests/Reflection/ReflectionGrpcServiceActivatorTests.cs b/test/Grpc.AspNetCore.Server.Tests/Reflection/ReflectionGrpcServiceActivatorTests.cs index 1b26d3652..f1b187caa 100644 --- a/test/Grpc.AspNetCore.Server.Tests/Reflection/ReflectionGrpcServiceActivatorTests.cs +++ b/test/Grpc.AspNetCore.Server.Tests/Reflection/ReflectionGrpcServiceActivatorTests.cs @@ -26,6 +26,7 @@ using Grpc.AspNetCore.Server.Reflection.Internal; using Grpc.Core; using Grpc.Reflection.V1Alpha; +using Grpc.Tests.Shared; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; diff --git a/test/Grpc.NetCore.HttpClient.Tests/AsyncClientStreamingCallTests.cs b/test/Grpc.NetCore.HttpClient.Tests/AsyncClientStreamingCallTests.cs index 1638222f7..b90dd51d8 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/AsyncClientStreamingCallTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/AsyncClientStreamingCallTests.cs @@ -26,7 +26,7 @@ using Grpc.Core; using Grpc.NetCore.HttpClient.Internal; using Grpc.NetCore.HttpClient.Tests.Infrastructure; -using Grpc.Tests; +using Grpc.Tests.Shared; using NUnit.Framework; namespace Grpc.NetCore.HttpClient.Tests diff --git a/test/Grpc.NetCore.HttpClient.Tests/AsyncDuplexStreamingCallTests.cs b/test/Grpc.NetCore.HttpClient.Tests/AsyncDuplexStreamingCallTests.cs index 919735882..b806d8568 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/AsyncDuplexStreamingCallTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/AsyncDuplexStreamingCallTests.cs @@ -25,7 +25,7 @@ using Grpc.Core; using Grpc.NetCore.HttpClient.Internal; using Grpc.NetCore.HttpClient.Tests.Infrastructure; -using Grpc.Tests; +using Grpc.Tests.Shared; using NUnit.Framework; namespace Grpc.NetCore.HttpClient.Tests @@ -160,6 +160,9 @@ await streamContent.AddDataAndWait(await TestHelpers.GetResponseDataAsync(new He await streamContent.AddDataAndWait(Array.Empty()).DefaultTimeout(); Assert.IsFalse(await moveNextTask3.DefaultTimeout()); + + var moveNextTask4 = responseStream.MoveNext(CancellationToken.None); + Assert.IsTrue(moveNextTask4.IsCompleted); Assert.IsFalse(await moveNextTask3.DefaultTimeout()); } } diff --git a/test/Grpc.NetCore.HttpClient.Tests/AsyncServerStreamingCallTests.cs b/test/Grpc.NetCore.HttpClient.Tests/AsyncServerStreamingCallTests.cs index b041d238f..3ce54c09a 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/AsyncServerStreamingCallTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/AsyncServerStreamingCallTests.cs @@ -24,7 +24,7 @@ using Greet; using Grpc.Core; using Grpc.NetCore.HttpClient.Tests.Infrastructure; -using Grpc.Tests; +using Grpc.Tests.Shared; using NUnit.Framework; namespace Grpc.NetCore.HttpClient.Tests @@ -151,6 +151,9 @@ await streamContent.AddDataAndWait(await TestHelpers.GetResponseDataAsync(new He await streamContent.AddDataAndWait(Array.Empty()).DefaultTimeout(); Assert.IsFalse(await moveNextTask3.DefaultTimeout()); + + var moveNextTask4 = responseStream.MoveNext(CancellationToken.None); + Assert.IsTrue(moveNextTask4.IsCompleted); Assert.IsFalse(await moveNextTask3.DefaultTimeout()); } } diff --git a/test/Grpc.NetCore.HttpClient.Tests/AsyncUnaryCallTests.cs b/test/Grpc.NetCore.HttpClient.Tests/AsyncUnaryCallTests.cs index 9125904a4..bfe7bd375 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/AsyncUnaryCallTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/AsyncUnaryCallTests.cs @@ -25,7 +25,7 @@ using Greet; using Grpc.Core; using Grpc.NetCore.HttpClient.Tests.Infrastructure; -using Grpc.Tests; +using Grpc.Tests.Shared; using NUnit.Framework; namespace Grpc.NetCore.HttpClient.Tests diff --git a/test/Grpc.NetCore.HttpClient.Tests/CancellationTests.cs b/test/Grpc.NetCore.HttpClient.Tests/CancellationTests.cs index dcd780f17..c8eca0931 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/CancellationTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/CancellationTests.cs @@ -32,7 +32,7 @@ using System.Text; using System.Linq; using Grpc.NetCore.HttpClient.Internal; -using Grpc.Tests; +using Grpc.Tests.Shared; namespace Grpc.NetCore.HttpClient.Tests { diff --git a/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs b/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs index 67f802e99..2f9691f25 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs @@ -28,7 +28,7 @@ using Grpc.Core; using Grpc.NetCore.HttpClient.Internal; using Grpc.NetCore.HttpClient.Tests.Infrastructure; -using Grpc.Tests; +using Grpc.Tests.Shared; using NUnit.Framework; namespace Grpc.NetCore.HttpClient.Tests @@ -186,8 +186,6 @@ public void AsyncClientStreamingCall_DeadlineDuringWrite_ResponseThrowsDeadlineE public void AsyncServerStreamingCall_DeadlineDuringWrite_ResponseThrowsDeadlineExceededStatus() { // Arrange - var cts = new CancellationTokenSource(); - var httpClient = TestHelpers.CreateTestClient(request => { var stream = new SyncPointMemoryStream(); diff --git a/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs b/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs index 0ed27f960..8366be377 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs @@ -32,7 +32,7 @@ using System.Text; using System.Linq; using Grpc.NetCore.HttpClient.Internal; -using Grpc.Tests; +using Grpc.Tests.Shared; namespace Grpc.NetCore.HttpClient.Tests { diff --git a/test/Grpc.NetCore.HttpClient.Tests/HeadersTests.cs b/test/Grpc.NetCore.HttpClient.Tests/HeadersTests.cs index 803587e26..0e39913dc 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/HeadersTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/HeadersTests.cs @@ -27,7 +27,7 @@ using Greet; using Grpc.Core; using Grpc.NetCore.HttpClient.Tests.Infrastructure; -using Grpc.Tests; +using Grpc.Tests.Shared; using NUnit.Framework; namespace Grpc.NetCore.HttpClient.Tests diff --git a/test/Grpc.NetCore.HttpClient.Tests/HttpContextClientStreamReaderTests.cs b/test/Grpc.NetCore.HttpClient.Tests/HttpContextClientStreamReaderTests.cs new file mode 100644 index 000000000..96e9daf2d --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/HttpContextClientStreamReaderTests.cs @@ -0,0 +1,89 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Greet; +using Grpc.Core; +using Grpc.NetCore.HttpClient.Internal; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using Grpc.Tests.Shared; +using NUnit.Framework; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class HttpContextClientStreamReaderTests + { + [Test] + public void MoveNext_TokenCanceledBeforeCall_ThrowError() + { + // Arrange + var cts = new CancellationTokenSource(); + cts.Cancel(); + + var httpClient = TestHelpers.CreateTestClient(request => + { + var stream = new SyncPointMemoryStream(); + var content = new StreamContent(stream); + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, content)); + }); + + var call = new GrpcCall(TestHelpers.ServiceMethod, new CallOptions(), SystemClock.Instance); + call.SendServerStreaming(httpClient, new HelloRequest()); + + // Act + var moveNextTask1 = call.ClientStreamReader.MoveNext(cts.Token); + + // Assert + Assert.IsTrue(moveNextTask1.IsCompleted); + var ex = Assert.ThrowsAsync(async () => await moveNextTask1.DefaultTimeout()); + Assert.AreEqual(StatusCode.Cancelled, ex.StatusCode); + } + + [Test] + public void MoveNext_TokenCanceledDuringCall_ThrowError() + { + // Arrange + var cts = new CancellationTokenSource(); + + var httpClient = TestHelpers.CreateTestClient(request => + { + var stream = new SyncPointMemoryStream(); + var content = new StreamContent(stream); + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, content)); + }); + + var call = new GrpcCall(TestHelpers.ServiceMethod, new CallOptions(), SystemClock.Instance); + call.SendServerStreaming(httpClient, new HelloRequest()); + + // Act + var moveNextTask1 = call.ClientStreamReader.MoveNext(cts.Token); + + // Assert + Assert.IsFalse(moveNextTask1.IsCompleted); + + cts.Cancel(); + + var ex = Assert.ThrowsAsync(async () => await moveNextTask1.DefaultTimeout()); + Assert.AreEqual(StatusCode.Cancelled, ex.StatusCode); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/ResponseAsyncTests.cs b/test/Grpc.NetCore.HttpClient.Tests/ResponseAsyncTests.cs index edab5b484..3d101d85e 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/ResponseAsyncTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/ResponseAsyncTests.cs @@ -24,7 +24,7 @@ using Greet; using Grpc.Core; using Grpc.NetCore.HttpClient.Tests.Infrastructure; -using Grpc.Tests; +using Grpc.Tests.Shared; using NUnit.Framework; namespace Grpc.NetCore.HttpClient.Tests diff --git a/test/Grpc.NetCore.HttpClient.Tests/ResponseHeadersAsyncTests.cs b/test/Grpc.NetCore.HttpClient.Tests/ResponseHeadersAsyncTests.cs index 8b631226c..2f758d589 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/ResponseHeadersAsyncTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/ResponseHeadersAsyncTests.cs @@ -26,7 +26,7 @@ using Greet; using Grpc.Core; using Grpc.NetCore.HttpClient.Tests.Infrastructure; -using Grpc.Tests; +using Grpc.Tests.Shared; using NUnit.Framework; namespace Grpc.NetCore.HttpClient.Tests diff --git a/test/Shared/HttpContextServerCallContextHelpers.cs b/test/Shared/HttpContextServerCallContextHelpers.cs index c9b5e8369..7e7439328 100644 --- a/test/Shared/HttpContextServerCallContextHelpers.cs +++ b/test/Shared/HttpContextServerCallContextHelpers.cs @@ -22,7 +22,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Grpc.AspNetCore.FunctionalTests.Infrastructure +namespace Grpc.Tests.Shared { internal static class HttpContextServerCallContextHelper { diff --git a/test/Shared/MessageHelpers.cs b/test/Shared/MessageHelpers.cs index 7b0b51b0d..fdc51558d 100644 --- a/test/Shared/MessageHelpers.cs +++ b/test/Shared/MessageHelpers.cs @@ -27,7 +27,7 @@ using Grpc.AspNetCore.Server.Internal; using Microsoft.AspNetCore.Http; -namespace Grpc.AspNetCore.FunctionalTests.Infrastructure +namespace Grpc.Tests.Shared { internal static class MessageHelpers { diff --git a/test/Shared/SyncPoint.cs b/test/Shared/SyncPoint.cs index 2ab83eee8..0269812dc 100644 --- a/test/Shared/SyncPoint.cs +++ b/test/Shared/SyncPoint.cs @@ -20,7 +20,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Grpc.Tests +namespace Grpc.Tests.Shared { public class SyncPoint { diff --git a/test/Shared/SyncPointMemoryStream.cs b/test/Shared/SyncPointMemoryStream.cs index 78b77f625..cfd81d1cf 100644 --- a/test/Shared/SyncPointMemoryStream.cs +++ b/test/Shared/SyncPointMemoryStream.cs @@ -22,7 +22,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Grpc.Tests +namespace Grpc.Tests.Shared { /// /// A memory stream that waits for data when reading and allows the sender of data to wait for it to be read. diff --git a/test/Shared/TaskExtensions.cs b/test/Shared/TaskExtensions.cs index 3267ffbf9..ea708499e 100644 --- a/test/Shared/TaskExtensions.cs +++ b/test/Shared/TaskExtensions.cs @@ -22,7 +22,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Grpc.Tests +namespace Grpc.Tests.Shared { internal static class TaskExtensions { From c1b653f28394e85fed388e545726902c005af860 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 26 Apr 2019 20:58:38 +1200 Subject: [PATCH 05/13] Clean up --- Directory.Build.props | 2 +- .../BenchmarkServer/BenchmarkServer.csproj | 2 +- .../Internal/EmptyContent.cs | 39 ------------------- .../Internal/GrpcProtocolConstants.cs | 7 ---- 4 files changed, 2 insertions(+), 48 deletions(-) delete mode 100644 src/Grpc.NetCore.HttpClient/Internal/EmptyContent.cs diff --git a/Directory.Build.props b/Directory.Build.props index c536a913b..7d05141e9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,7 +10,7 @@ $(WarningsNotAsErrors);CS1591 - + preview diff --git a/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj b/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj index 4ab4e4c23..a5788cf7c 100644 --- a/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj +++ b/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj @@ -34,6 +34,6 @@ - + diff --git a/src/Grpc.NetCore.HttpClient/Internal/EmptyContent.cs b/src/Grpc.NetCore.HttpClient/Internal/EmptyContent.cs deleted file mode 100644 index d94ebf39b..000000000 --- a/src/Grpc.NetCore.HttpClient/Internal/EmptyContent.cs +++ /dev/null @@ -1,39 +0,0 @@ -#region Copyright notice and license - -// Copyright 2019 The gRPC Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#endregion - -using System.IO; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; - -namespace Grpc.NetCore.HttpClient.Internal -{ - internal class EmptyContent : HttpContent - { - protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) - { - return Task.CompletedTask; - } - - protected override bool TryComputeLength(out long length) - { - length = -1; - return false; - } - } -} diff --git a/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolConstants.cs b/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolConstants.cs index d36f00990..d786d2322 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolConstants.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolConstants.cs @@ -24,13 +24,6 @@ internal static class GrpcProtocolConstants { internal const string GrpcContentType = "application/grpc"; internal static readonly MediaTypeHeaderValue GrpcContentTypeHeaderValue = new MediaTypeHeaderValue("application/grpc"); - internal static readonly EmptyContent EmptyGrpcContent; - - static GrpcProtocolConstants() - { - EmptyGrpcContent = new EmptyContent(); - EmptyGrpcContent.Headers.ContentType = GrpcContentTypeHeaderValue; - } internal const string TimeoutHeader = "grpc-timeout"; internal const string MessageEncodingHeader = "grpc-encoding"; From 32581b0bf637f7655129f68b0cfe2af15267f308 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 26 Apr 2019 22:45:07 +1200 Subject: [PATCH 06/13] C Core deadline formatting --- .../Internal/GrpcCall.cs | 3 +- .../Internal/GrpcProtocolHelpers.cs | 77 +++++++++++++++++++ .../DeadlineTests.cs | 4 +- .../GrpcProtocolHelpersTests.cs | 59 ++++++++++++++ 4 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 test/Grpc.NetCore.HttpClient.Tests/GrpcProtocolHelpersTests.cs diff --git a/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs index 1fb83f74f..cd0345b79 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs @@ -201,8 +201,7 @@ private HttpRequestMessage CreateHttpRequestMessage() if (_timeout != null) { - // JamesNK(todo) - Replicate C core's logic for formatting grpc-timeout - message.Headers.Add(GrpcProtocolConstants.TimeoutHeader, Convert.ToInt64(_timeout.Value.TotalMilliseconds) + "m"); + message.Headers.Add(GrpcProtocolConstants.TimeoutHeader, GrpcProtocolHelpers.EncodeTimeout(Convert.ToInt64(_timeout.Value.TotalMilliseconds))); } return message; diff --git a/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolHelpers.cs b/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolHelpers.cs index 1a7b2116a..df354592f 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolHelpers.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolHelpers.cs @@ -107,5 +107,82 @@ public static Metadata BuildMetadata(HttpResponseHeaders responseHeaders) return headers; } + + private const int MillisecondsPerSecond = 1000; + + /* round an integer up to the next value with three significant figures */ + private static long TimeoutRoundUpToThreeSignificantFigures(long x) + { + if (x < 1000) return x; + if (x < 10000) return RoundUp(x, 10); + if (x < 100000) return RoundUp(x, 100); + if (x < 1000000) return RoundUp(x, 1000); + if (x < 10000000) return RoundUp(x, 10000); + if (x < 100000000) return RoundUp(x, 100000); + if (x < 1000000000) return RoundUp(x, 1000000); + return RoundUp(x, 10000000); + + static long RoundUp(long x, long divisor) + { + return (x / divisor + Convert.ToInt32(x % divisor != 0)) * divisor; + } + } + + private static string FormatTimeout(long value, char ext) + { + return value.ToString() + ext; + } + + private static string EncodeTimeoutSeconds(long sec) + { + if (sec % 3600 == 0) + { + return FormatTimeout(sec / 3600, 'H'); + } + else if (sec % 60 == 0) + { + return FormatTimeout(sec / 60, 'M'); + } + else + { + return FormatTimeout(sec, 'S'); + } + } + + private static string EncodeTimeoutMilliseconds(long x) + { + x = TimeoutRoundUpToThreeSignificantFigures(x); + if (x < MillisecondsPerSecond) + { + return FormatTimeout(x, 'm'); + } + else + { + if (x % MillisecondsPerSecond == 0) + { + return EncodeTimeoutSeconds(x / MillisecondsPerSecond); + } + else + { + return FormatTimeout(x, 'm'); + } + } + } + + public static string EncodeTimeout(long timeout) + { + if (timeout <= 0) + { + return "1n"; + } + else if (timeout < 1000 * MillisecondsPerSecond) + { + return EncodeTimeoutMilliseconds(timeout); + } + else + { + return EncodeTimeoutSeconds(timeout / MillisecondsPerSecond + Convert.ToInt32(timeout % MillisecondsPerSecond != 0)); + } + } } } diff --git a/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs b/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs index 2f9691f25..54006acc8 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs @@ -58,7 +58,7 @@ public async Task AsyncUnaryCall_SetSecondDeadline_RequestMessageContainsDeadlin // Assert Assert.IsNotNull(httpRequestMessage); Assert.AreEqual(1, httpRequestMessage.Headers.Count()); - Assert.AreEqual("1000m", httpRequestMessage.Headers.GetValues("grpc-timeout").Single()); + Assert.AreEqual("1S", httpRequestMessage.Headers.GetValues("grpc-timeout").Single()); } [Test] @@ -117,7 +117,7 @@ public async Task AsyncUnaryCall_SendDeadlineHeaderAndDeadlineValue_DeadlineValu Assert.IsNotNull(httpRequestMessage); Assert.AreEqual(1, httpRequestMessage.Headers.Count()); - Assert.AreEqual("1000m", httpRequestMessage.Headers.GetValues("grpc-timeout").Single()); + Assert.AreEqual("1S", httpRequestMessage.Headers.GetValues("grpc-timeout").Single()); } [Test] diff --git a/test/Grpc.NetCore.HttpClient.Tests/GrpcProtocolHelpersTests.cs b/test/Grpc.NetCore.HttpClient.Tests/GrpcProtocolHelpersTests.cs new file mode 100644 index 000000000..1ca736199 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/GrpcProtocolHelpersTests.cs @@ -0,0 +1,59 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using NUnit.Framework; +using Greet; +using static Greet.Greeter; +using System; +using Grpc.NetCore.HttpClient.Internal; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class GrpcProtocolHelpersTests + { + private const int MillisecondsPerSecond = 1000; + + [TestCase(-1, "1n")] + [TestCase(-10, "1n")] + [TestCase(1, "1m")] + [TestCase(10, "10m")] + [TestCase(100, "100m")] + [TestCase(890, "890m")] + [TestCase(900, "900m")] + [TestCase(901, "901m")] + [TestCase(1000, "1S")] + [TestCase(2000, "2S")] + [TestCase(2500, "2500m")] + [TestCase(59900, "59900m")] + [TestCase(50000, "50S")] + [TestCase(59000, "59S")] + [TestCase(60000, "1M")] + [TestCase(80000, "80S")] + [TestCase(90000, "90S")] + [TestCase(120000, "2M")] + [TestCase(20 * 60 * MillisecondsPerSecond, "20M")] + [TestCase(60 * 60 * MillisecondsPerSecond, "1H")] + [TestCase(10 * 60 * 60 * MillisecondsPerSecond, "10H")] + public void EncodeTimeout(int milliseconds, string expected) + { + var encoded = GrpcProtocolHelpers.EncodeTimeout(milliseconds); + Assert.AreEqual(expected, encoded); + } + } +} From 2faf00da3df79270803ffa7eaea60e7ead2a92a8 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 26 Apr 2019 22:55:26 +1200 Subject: [PATCH 07/13] Flaky test --- test/FunctionalTests/DuplexStreamingMethodTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/FunctionalTests/DuplexStreamingMethodTests.cs b/test/FunctionalTests/DuplexStreamingMethodTests.cs index 76a666cac..ad62c0855 100644 --- a/test/FunctionalTests/DuplexStreamingMethodTests.cs +++ b/test/FunctionalTests/DuplexStreamingMethodTests.cs @@ -65,7 +65,6 @@ public async Task MultipleMessagesFromOneClient_SuccessResponses() var pipeReader = new StreamPipeReader(responseStream); var message1Task = MessageHelpers.AssertReadStreamMessageAsync(pipeReader); - Assert.IsTrue(message1Task.IsCompleted); var message1 = await message1Task.DefaultTimeout(); Assert.AreEqual("John", message1.Name); Assert.AreEqual("Hello Jill", message1.Message); From 9812a63ea2aca64b4059594465c35690ddf379db Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sat, 27 Apr 2019 23:31:37 +1200 Subject: [PATCH 08/13] Validate response HTTP headers --- .../HttpClientCallInvoker.cs | 8 +- .../Internal/GrpcCall.cs | 333 +++++++++++------- .../Internal/HttpContextClientStreamReader.cs | 8 +- .../Internal/StreamExtensions.cs | 8 +- .../AsyncClientStreamingCallTests.cs | 56 +-- .../AsyncServerStreamingCallTests.cs | 20 ++ .../CancellationTests.cs | 64 +++- .../DeadlineTests.cs | 37 +- .../GetStatusTests.cs | 112 ++++++ .../GetTrailersTests.cs | 41 ++- .../HttpContextClientStreamReaderTests.cs | 4 +- .../Infrastructure/ResponseUtils.cs | 5 +- .../ResponseAsyncTests.cs | 43 ++- .../ResponseHeadersAsyncTests.cs | 41 ++- 14 files changed, 598 insertions(+), 182 deletions(-) create mode 100644 test/Grpc.NetCore.HttpClient.Tests/GetStatusTests.cs diff --git a/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs b/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs index 96ae3afe6..1e9f37f2f 100644 --- a/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs +++ b/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs @@ -74,7 +74,7 @@ public HttpClientCallInvoker(System.Net.Http.HttpClient client) public override AsyncClientStreamingCall AsyncClientStreamingCall(Method method, string host, CallOptions options) { var call = CreateGrpcCall(method, options); - call.SendClientStreaming(_client); + call.StartClientStreaming(_client); return new AsyncClientStreamingCall( requestStream: call.ClientStreamWriter, @@ -93,7 +93,7 @@ public override AsyncClientStreamingCall AsyncClientStreami public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall(Method method, string host, CallOptions options) { var call = CreateGrpcCall(method, options); - call.SendDuplexStreaming(_client); + call.StartDuplexStreaming(_client); return new AsyncDuplexStreamingCall( requestStream: call.ClientStreamWriter, @@ -111,7 +111,7 @@ public override AsyncDuplexStreamingCall AsyncDuplexStreami public override AsyncServerStreamingCall AsyncServerStreamingCall(Method method, string host, CallOptions options, TRequest request) { var call = CreateGrpcCall(method, options); - call.SendServerStreaming(_client, request); + call.StartServerStreaming(_client, request); return new AsyncServerStreamingCall( responseStream: call.ClientStreamReader, @@ -127,7 +127,7 @@ public override AsyncServerStreamingCall AsyncServerStreamingCall AsyncUnaryCall(Method method, string host, CallOptions options, TRequest request) { var call = CreateGrpcCall(method, options); - call.SendUnary(_client, request); + call.StartUnary(_client, request); return new AsyncUnaryCall( responseAsync: call.GetResponseAsync(), diff --git a/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs index cd0345b79..1eeee9a1b 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs @@ -19,8 +19,8 @@ using System; using System.IO; using System.Linq; +using System.Net; using System.Net.Http; -using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using Grpc.Core; @@ -34,17 +34,17 @@ internal class GrpcCall private readonly ISystemClock _clock; private readonly TimeSpan? _timeout; private readonly Timer _deadlineTimer; - - private HttpResponseMessage _httpResponse; private Metadata _trailers; private CancellationTokenRegistration? _writerCtsRegistration; + private string _headerValidationError; public bool DeadlineReached { get; private set; } public bool Disposed { get; private set; } public bool ResponseFinished { get; private set; } + public HttpResponseMessage HttpResponse { get; private set; } public CallOptions Options { get; } public Method Method { get; } - public Task SendTask { get; private set; } + public Task SendTask { get; private set; } public HttpContentClientStreamWriter ClientStreamWriter { get; private set; } public HttpContextClientStreamReader ClientStreamReader { get; private set; } @@ -57,6 +57,7 @@ public GrpcCall(Method method, CallOptions options, ISystem if (options.CancellationToken.CanBeCanceled) { + // The cancellation token will cancel the call CTS _ctsRegistration = options.CancellationToken.Register(CancelCall); } @@ -68,70 +69,48 @@ public GrpcCall(Method method, CallOptions options, ISystem if (_timeout != null) { - _deadlineTimer = new Timer(ReachDeadline, null, _timeout.Value, Timeout.InfiniteTimeSpan); - } - } - - private void ReachDeadline(object state) - { - if (!_callCts.IsCancellationRequested) - { - DeadlineReached = true; - _callCts.Cancel(); + // Deadline timer will cancel the call CTS + _deadlineTimer = new Timer(DeadlineExceeded, null, _timeout.Value, Timeout.InfiniteTimeSpan); } } - private void CancelCall() - { - _callCts.Cancel(); - } - public CancellationToken CancellationToken { get { return _callCts.Token; } } - public void SendUnary(System.Net.Http.HttpClient client, TRequest request) + public bool IsCancellationRequested { - var message = CreateHttpRequestMessage(); - SetMessageContent(request, message); - SendCore(client, message); + get { return _callCts.IsCancellationRequested; } } - private void SetMessageContent(TRequest request, HttpRequestMessage message) + public void StartUnary(System.Net.Http.HttpClient client, TRequest request) { - message.Content = new PushStreamContent( - (stream) => - { - return SerializationHelpers.WriteMessage(stream, request, Method.RequestMarshaller.Serializer, Options.CancellationToken); - }, - GrpcProtocolConstants.GrpcContentTypeHeaderValue); + var message = CreateHttpRequestMessage(); + SetMessageContent(request, message); + StartSend(client, message); } - public void SendClientStreaming(System.Net.Http.HttpClient client) + public void StartClientStreaming(System.Net.Http.HttpClient client) { var message = CreateHttpRequestMessage(); ClientStreamWriter = CreateWriter(message); - - SendCore(client, message); + StartSend(client, message); } - public void SendServerStreaming(System.Net.Http.HttpClient client, TRequest request) + public void StartServerStreaming(System.Net.Http.HttpClient client, TRequest request) { var message = CreateHttpRequestMessage(); SetMessageContent(request, message); - SendCore(client, message); - + StartSend(client, message); ClientStreamReader = new HttpContextClientStreamReader(this); } - public void SendDuplexStreaming(System.Net.Http.HttpClient client) + public void StartDuplexStreaming(System.Net.Http.HttpClient client) { var message = CreateHttpRequestMessage(); ClientStreamWriter = CreateWriter(message); - - SendCore(client, message); - + StartSend(client, message); ClientStreamReader = new HttpContextClientStreamReader(this); } @@ -146,142 +125,246 @@ public void Dispose() _ctsRegistration?.Dispose(); _writerCtsRegistration?.Dispose(); _deadlineTimer?.Dispose(); - _httpResponse?.Dispose(); + HttpResponse?.Dispose(); ClientStreamReader?.Dispose(); ClientStreamWriter?.Dispose(); } } - private void SendCore(System.Net.Http.HttpClient client, HttpRequestMessage message) + public void EnsureNotDisposed() { - SendTask = client.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, _callCts.Token); + if (Disposed) + { + throw new ObjectDisposedException(nameof(GrpcCall)); + } } - private HttpContentClientStreamWriter CreateWriter(HttpRequestMessage message) + public void EnsureHeadersValid() { - var writeStreamTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var completeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - _writerCtsRegistration = _callCts.Token.Register(() => + if (_headerValidationError != null) { - completeTcs.TrySetCanceled(); - writeStreamTcs.TrySetCanceled(); - }); + throw new InvalidOperationException(_headerValidationError); + } + } - message.Content = new PushStreamContent( - (stream) => - { - writeStreamTcs.SetResult(stream); - return completeTcs.Task; - }, - GrpcProtocolConstants.GrpcContentTypeHeaderValue); + public Exception CreateCanceledStatusException() + { + if (_headerValidationError != null) + { + return new InvalidOperationException(_headerValidationError); + } - var writer = new HttpContentClientStreamWriter(this, writeStreamTcs.Task, completeTcs); - return writer; + var statusCode = DeadlineReached ? StatusCode.DeadlineExceeded : StatusCode.Cancelled; + return new RpcException(new Status(statusCode, string.Empty)); } - private HttpRequestMessage CreateHttpRequestMessage() + public void FinishResponse(HttpResponseMessage httpResponseMessage) { - var message = new HttpRequestMessage(HttpMethod.Post, Method.FullName); - message.Version = new Version(2, 0); - - if (Options.Headers != null && Options.Headers.Count > 0) + if (ResponseFinished) { - foreach (var entry in Options.Headers) - { - // Deadline is set via CallOptions.Deadline - if (entry.Key == GrpcProtocolConstants.TimeoutHeader) - { - continue; - } - - var value = entry.IsBinary ? Convert.ToBase64String(entry.ValueBytes) : entry.Value; - message.Headers.Add(entry.Key, value); - } + return; } - if (_timeout != null) + ResponseFinished = true; + + // Clean up call resources once this call is finished + // Call may not be explicitly disposed when used with unary methods + // e.g. var reply = await client.SayHelloAsync(new HelloRequest()); + Dispose(); + + HttpResponse = httpResponseMessage; + + var status = GetStatusCore(HttpResponse); + if (status.StatusCode != StatusCode.OK) { - message.Headers.Add(GrpcProtocolConstants.TimeoutHeader, GrpcProtocolHelpers.EncodeTimeout(Convert.ToInt64(_timeout.Value.TotalMilliseconds))); + throw new RpcException(status); } - - return message; } - public void EnsureNotDisposed() + public async Task GetResponseHeadersAsync() { - if (Disposed) + try { - throw new ObjectDisposedException(nameof(GrpcCall)); + await SendTask.ConfigureAwait(false); + + // The task of this method is cached so there is no need to cache the headers here + return GrpcProtocolHelpers.BuildMetadata(HttpResponse.Headers); } + catch (OperationCanceledException) + { + EnsureNotDisposed(); + throw CreateCanceledStatusException(); + } + } + + public Status GetStatus() + { + ValidateTrailersAvailable(); + + return GetStatusCore(HttpResponse); } public async Task GetResponseAsync() { try { - _httpResponse = await SendTask.ConfigureAwait(false); + await SendTask.ConfigureAwait(false); - // Server might have returned a status without any response body. For example, an unimplemented status - // Check for the trailer status before attempting to read the body and failing - if (_httpResponse.TrailingHeaders.Contains(GrpcProtocolConstants.StatusTrailer)) + // Trailers are only available once the response body had been read + var responseStream = await HttpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); + var message = await responseStream.ReadSingleMessageAsync(Method.ResponseMarshaller.Deserializer, _callCts.Token).ConfigureAwait(false); + FinishResponse(HttpResponse); + + if (message == null) { - FinishResponse(_httpResponse); + throw new InvalidOperationException("Call did not return a response message"); } - var responseStream = await _httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); - var message = await responseStream.ReadSingleMessageAsync(Method.ResponseMarshaller.Deserializer, _callCts.Token).ConfigureAwait(false); - FinishResponse(_httpResponse); - // The task of this method is cached so there is no need to cache the message here return message; } catch (OperationCanceledException) { + EnsureNotDisposed(); throw CreateCanceledStatusException(); } } - internal RpcException CreateCanceledStatusException() + private void ValidateHeaders() { - var statusCode = DeadlineReached ? StatusCode.DeadlineExceeded : StatusCode.Cancelled; - return new RpcException(new Status(statusCode, string.Empty)); + if (HttpResponse.StatusCode != HttpStatusCode.OK) + { + _headerValidationError = "Bad gRPC response. Expected HTTP status code 200. Got status code: " + (int)HttpResponse.StatusCode; + } + else if (HttpResponse.Content.Headers.ContentType == null) + { + _headerValidationError = "Bad gRPC response. Response did not have a content-type header."; + } + else + { + var grpcEncoding = HttpResponse.Content.Headers.ContentType.ToString(); + if (!GrpcProtocolHelpers.IsGrpcContentType(grpcEncoding)) + { + _headerValidationError = "Bad gRPC response. Invalid content-type value: " + grpcEncoding; + } + } + + if (_headerValidationError != null) + { + // Response is not valid gRPC + // Clean up/cancel any pending operations + Dispose(); + + throw new InvalidOperationException(_headerValidationError); + } + + // Success! } - internal void FinishResponse(HttpResponseMessage httpResponseMessage) + public Metadata GetTrailers() { - if (ResponseFinished) + if (_trailers == null) { - return; + ValidateTrailersAvailable(); + + _trailers = GrpcProtocolHelpers.BuildMetadata(HttpResponse.TrailingHeaders); } - ResponseFinished = true; + return _trailers; + } - _httpResponse = httpResponseMessage; + private void SetMessageContent(TRequest request, HttpRequestMessage message) + { + message.Content = new PushStreamContent( + (stream) => + { + return SerializationHelpers.WriteMessage(stream, request, Method.RequestMarshaller.Serializer, Options.CancellationToken); + }, + GrpcProtocolConstants.GrpcContentTypeHeaderValue); + } - var status = GetStatusCore(_httpResponse); - if (status.StatusCode != StatusCode.OK) + private void CancelCall() + { + _callCts.Cancel(); + } + + private void StartSend(System.Net.Http.HttpClient client, HttpRequestMessage message) + { + SendTask = SendAsync(client, message); + } + + private async Task SendAsync(System.Net.Http.HttpClient client, HttpRequestMessage message) + { + HttpResponse = await client.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, _callCts.Token).ConfigureAwait(false); + ValidateHeaders(); + } + + private HttpContentClientStreamWriter CreateWriter(HttpRequestMessage message) + { + var writeStreamTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var completeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + // Canceling call will cancel pending writes to the stream + _writerCtsRegistration = _callCts.Token.Register(() => { - throw new RpcException(status); - } + completeTcs.TrySetCanceled(); + writeStreamTcs.TrySetCanceled(); + }); + + message.Content = new PushStreamContent( + (stream) => + { + writeStreamTcs.SetResult(stream); + return completeTcs.Task; + }, + GrpcProtocolConstants.GrpcContentTypeHeaderValue); + + var writer = new HttpContentClientStreamWriter(this, writeStreamTcs.Task, completeTcs); + return writer; } - public async Task GetResponseHeadersAsync() + private HttpRequestMessage CreateHttpRequestMessage() { - _httpResponse = await SendTask.ConfigureAwait(false); + var message = new HttpRequestMessage(HttpMethod.Post, Method.FullName); + message.Version = new Version(2, 0); + + if (Options.Headers != null && Options.Headers.Count > 0) + { + foreach (var entry in Options.Headers) + { + // Deadline is set via CallOptions.Deadline + if (entry.Key == GrpcProtocolConstants.TimeoutHeader) + { + continue; + } + + var value = entry.IsBinary ? Convert.ToBase64String(entry.ValueBytes) : entry.Value; + message.Headers.Add(entry.Key, value); + } + } + + if (_timeout != null) + { + message.Headers.Add(GrpcProtocolConstants.TimeoutHeader, GrpcProtocolHelpers.EncodeTimeout(Convert.ToInt64(_timeout.Value.TotalMilliseconds))); + } - // The task of this method is cached so there is no need to cache the headers here - return GrpcProtocolHelpers.BuildMetadata(_httpResponse.Headers); + return message; } - public Status GetStatus() + private void DeadlineExceeded(object state) { - ValidateTrailersAvailable(); + if (!_callCts.IsCancellationRequested) + { + // Flag is used to determin status code when generating exceptions + DeadlineReached = true; - return GetStatusCore(_httpResponse); + _callCts.Cancel(); + } } private static Status GetStatusCore(HttpResponseMessage httpResponseMessage) { + // grpc-status is a required trailer string grpcStatus; if (!httpResponseMessage.TrailingHeaders.TryGetValues(GrpcProtocolConstants.StatusTrailer, out var grpcStatusValues) || (grpcStatus = grpcStatusValues.FirstOrDefault()) == null) @@ -295,6 +378,7 @@ private static Status GetStatusCore(HttpResponseMessage httpResponseMessage) throw new InvalidOperationException("Unexpected grpc-status value: " + grpcStatus); } + // grpc-message is optional string grpcMessage = null; if (httpResponseMessage.TrailingHeaders.TryGetValues(GrpcProtocolConstants.MessageTrailer, out var grpcMessageValues)) { @@ -305,34 +389,33 @@ private static Status GetStatusCore(HttpResponseMessage httpResponseMessage) return new Status((StatusCode)statusValue, grpcMessage); } - public Metadata GetTrailers() + private void ValidateTrailersAvailable() { - if (_trailers == null) - { - ValidateTrailersAvailable(); + // Response headers have been returned and are not a valid grpc response + EnsureHeadersValid(); - _trailers = GrpcProtocolHelpers.BuildMetadata(SendTask.Result.TrailingHeaders); + // Response is finished + if (ResponseFinished) + { + return; } - return _trailers; - } - - private void ValidateTrailersAvailable() - { // Async call could have been disposed EnsureNotDisposed(); + // Call could have been canceled or deadline exceeded + if (_callCts.IsCancellationRequested) + { + throw CreateCanceledStatusException(); + } + // HttpClient.SendAsync could have failed if (SendTask.IsFaulted) { throw new InvalidOperationException("Can't get the call trailers because an error occured when making the request.", SendTask.Exception); } - // Response could still be in progress - if (!ResponseFinished || !SendTask.IsCompletedSuccessfully) - { - throw new InvalidOperationException("Can't get the call trailers because the call is not complete."); - } + throw new InvalidOperationException("Can't get the call trailers because the call is not complete."); } } } diff --git a/src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs b/src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs index 3976cd5c2..c7ec45033 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs @@ -50,10 +50,7 @@ public async Task MoveNext(CancellationToken cancellationToken) return false; } - // User could have disposed call - _call.EnsureNotDisposed(); - - if (_call.CancellationToken.IsCancellationRequested) + if (_call.IsCancellationRequested) { throw _call.CreateCanceledStatusException(); } @@ -76,7 +73,8 @@ public async Task MoveNext(CancellationToken cancellationToken) if (_httpResponse == null) { - _httpResponse = await _call.SendTask.ConfigureAwait(false); + await _call.SendTask.ConfigureAwait(false); + _httpResponse = _call.HttpResponse; } if (_responseStream == null) { diff --git a/src/Grpc.NetCore.HttpClient/Internal/StreamExtensions.cs b/src/Grpc.NetCore.HttpClient/Internal/StreamExtensions.cs index 72d172508..28f066ecb 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/StreamExtensions.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/StreamExtensions.cs @@ -28,15 +28,15 @@ internal static class StreamExtensions { public static Task ReadSingleMessageAsync(this Stream responseStream, Func deserializer, CancellationToken cancellationToken) { - return responseStream.ReadMessageCoreAsync(deserializer, cancellationToken, true); + return responseStream.ReadMessageCoreAsync(deserializer, cancellationToken, true, true); } public static Task ReadStreamedMessageAsync(this Stream responseStream, Func deserializer, CancellationToken cancellationToken) { - return responseStream.ReadMessageCoreAsync(deserializer, cancellationToken, false); + return responseStream.ReadMessageCoreAsync(deserializer, cancellationToken, true, false); } - private static async Task ReadMessageCoreAsync(this Stream responseStream, Func deserializer, CancellationToken cancellationToken, bool singleMessage) + private static async Task ReadMessageCoreAsync(this Stream responseStream, Func deserializer, CancellationToken cancellationToken, bool canBeEmpty, bool singleMessage) { cancellationToken.ThrowIfCancellationRequested(); @@ -56,7 +56,7 @@ private static async Task ReadMessageCoreAsync(this Stream if (received < header.Length) { - if (received == 0 && !singleMessage) + if (received == 0 && canBeEmpty) { return default; } diff --git a/test/Grpc.NetCore.HttpClient.Tests/AsyncClientStreamingCallTests.cs b/test/Grpc.NetCore.HttpClient.Tests/AsyncClientStreamingCallTests.cs index b90dd51d8..0d7623199 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/AsyncClientStreamingCallTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/AsyncClientStreamingCallTests.cs @@ -124,14 +124,10 @@ public async Task AsyncClientStreamingCall_Success_RequestContentSent() public void ClientStreamWriter_WriteWhilePendingWrite_ErrorThrown() { // Arrange - var httpClient = TestHelpers.CreateTestClient(async request => + var httpClient = TestHelpers.CreateTestClient(request => { - var streamContent = await TestHelpers.CreateResponseContent(new HelloReply - { - Message = "Hello world" - }); - - return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + var streamContent = new StreamContent(new SyncPointMemoryStream()); + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent)); }); var invoker = new HttpClientCallInvoker(httpClient); @@ -143,7 +139,7 @@ public void ClientStreamWriter_WriteWhilePendingWrite_ErrorThrown() Assert.IsFalse(writeTask1.IsCompleted); var writeTask2 = call.RequestStream.WriteAsync(new HelloRequest { Name = "2" }); - var ex = Assert.ThrowsAsync(() => writeTask2); + var ex = Assert.ThrowsAsync(() => writeTask2.DefaultTimeout()); Assert.AreEqual("Cannot write message because the previous write is in progress.", ex.Message); } @@ -152,14 +148,10 @@ public void ClientStreamWriter_WriteWhilePendingWrite_ErrorThrown() public void ClientStreamWriter_CompleteWhilePendingWrite_ErrorThrown() { // Arrange - var httpClient = TestHelpers.CreateTestClient(async request => + var httpClient = TestHelpers.CreateTestClient(request => { - var streamContent = await TestHelpers.CreateResponseContent(new HelloReply - { - Message = "Hello world" - }); - - return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + var streamContent = new StreamContent(new SyncPointMemoryStream()); + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent)); }); var invoker = new HttpClientCallInvoker(httpClient); @@ -171,7 +163,7 @@ public void ClientStreamWriter_CompleteWhilePendingWrite_ErrorThrown() Assert.IsFalse(writeTask1.IsCompleted); var completeTask = call.RequestStream.CompleteAsync(); - var ex = Assert.ThrowsAsync(() => completeTask); + var ex = Assert.ThrowsAsync(() => completeTask.DefaultTimeout()); Assert.AreEqual("Cannot complete client stream writer because the previous write is in progress.", ex.Message); } @@ -180,14 +172,10 @@ public void ClientStreamWriter_CompleteWhilePendingWrite_ErrorThrown() public async Task ClientStreamWriter_WriteWhileComplete_ErrorThrown() { // Arrange - var httpClient = TestHelpers.CreateTestClient(async request => + var httpClient = TestHelpers.CreateTestClient(request => { - var streamContent = await TestHelpers.CreateResponseContent(new HelloReply - { - Message = "Hello world" - }); - - return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + var streamContent = new StreamContent(new SyncPointMemoryStream()); + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent)); }); var invoker = new HttpClientCallInvoker(httpClient); @@ -196,9 +184,29 @@ public async Task ClientStreamWriter_WriteWhileComplete_ErrorThrown() await call.RequestStream.CompleteAsync(); // Assert - var ex = Assert.ThrowsAsync(() => call.RequestStream.WriteAsync(new HelloRequest { Name = "1" })); + var ex = Assert.ThrowsAsync(() => call.RequestStream.WriteAsync(new HelloRequest { Name = "1" }).DefaultTimeout()); Assert.AreEqual("Cannot write message because the client stream writer is complete.", ex.Message); } + + [Test] + public void ClientStreamWriter_WriteWithInvalidHttpStatus_ErrorThrown() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var streamContent = new StreamContent(new SyncPointMemoryStream()); + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.NotFound, streamContent)); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + + // Assert + var ex = Assert.ThrowsAsync(() => call.RequestStream.WriteAsync(new HelloRequest { Name = "1" }).DefaultTimeout()); + + Assert.AreEqual("Bad gRPC response. Expected HTTP status code 200. Got status code: 404", ex.Message); + } } } diff --git a/test/Grpc.NetCore.HttpClient.Tests/AsyncServerStreamingCallTests.cs b/test/Grpc.NetCore.HttpClient.Tests/AsyncServerStreamingCallTests.cs index 3ce54c09a..9d71fc2be 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/AsyncServerStreamingCallTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/AsyncServerStreamingCallTests.cs @@ -156,5 +156,25 @@ await streamContent.AddDataAndWait(await TestHelpers.GetResponseDataAsync(new He Assert.IsTrue(moveNextTask4.IsCompleted); Assert.IsFalse(await moveNextTask3.DefaultTimeout()); } + + [Test] + public void ClientStreamReader_WriteWithInvalidHttpStatus_ErrorThrown() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var streamContent = new StreamContent(new SyncPointMemoryStream()); + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.NotFound, streamContent)); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + + // Assert + var ex = Assert.ThrowsAsync(async () => await call.ResponseStream.MoveNext(CancellationToken.None).DefaultTimeout()); + + Assert.AreEqual("Bad gRPC response. Expected HTTP status code 200. Got status code: 404", ex.Message); + } } } diff --git a/test/Grpc.NetCore.HttpClient.Tests/CancellationTests.cs b/test/Grpc.NetCore.HttpClient.Tests/CancellationTests.cs index c8eca0931..833b60e80 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/CancellationTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/CancellationTests.cs @@ -44,16 +44,7 @@ public void AsyncClientStreamingCall_CancellationDuringSend_ResponseThrowsCancel { // Arrange var cts = new CancellationTokenSource(); - PushStreamContent content = null; - - var httpClient = TestHelpers.CreateTestClient(async request => - { - content = (PushStreamContent)request.Content; - await content.PushComplete.DefaultTimeout(); - - return ResponseUtils.CreateResponse(HttpStatusCode.OK); - }); - var invoker = new HttpClientCallInvoker(httpClient); + var invoker = CreateTimedoutCallInvoker(); // Act var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(cancellationToken: cts.Token)); @@ -67,5 +58,58 @@ public void AsyncClientStreamingCall_CancellationDuringSend_ResponseThrowsCancel var ex = Assert.ThrowsAsync(async () => await responseTask.DefaultTimeout()); Assert.AreEqual(StatusCode.Cancelled, ex.Status.StatusCode); } + + [Test] + public void AsyncClientStreamingCall_CancellationDuringSend_ResponseHeadersThrowsCancelledStatus() + { + // Arrange + var cts = new CancellationTokenSource(); + var invoker = CreateTimedoutCallInvoker(); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(cancellationToken: cts.Token)); + + // Assert + var responseHeadersTask = call.ResponseHeadersAsync; + Assert.IsFalse(responseHeadersTask.IsCompleted, "Headers not returned until client stream is complete."); + + cts.Cancel(); + + var ex = Assert.ThrowsAsync(async () => await responseHeadersTask.DefaultTimeout()); + Assert.AreEqual(StatusCode.Cancelled, ex.Status.StatusCode); + } + + [Test] + public void AsyncClientStreamingCall_CancellationDuringSend_TrailersThrowsCancelledStatus() + { + // Arrange + var cts = new CancellationTokenSource(); + var invoker = CreateTimedoutCallInvoker(); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(cancellationToken: cts.Token)); + + // Assert + cts.Cancel(); + + var ex = Assert.Throws(() => call.GetTrailers()); + + Assert.AreEqual(StatusCode.Cancelled, ex.Status.StatusCode); + } + + private static HttpClientCallInvoker CreateTimedoutCallInvoker() + { + PushStreamContent content = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + content = (PushStreamContent)request.Content; + await content.PushComplete.DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK); + }); + var invoker = new HttpClientCallInvoker(httpClient); + return invoker; + } } } diff --git a/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs b/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs index 54006acc8..a4c5ebae6 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs @@ -170,7 +170,9 @@ public void AsyncClientStreamingCall_DeadlineDuringWrite_ResponseThrowsDeadlineE // Arrange var httpClient = TestHelpers.CreateTestClient(request => { - return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK)); + var stream = new SyncPointMemoryStream(); + var content = new StreamContent(stream); + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, content, grpcStatusCode: null)); }); var invoker = new HttpClientCallInvoker(httpClient); @@ -202,6 +204,39 @@ public void AsyncServerStreamingCall_DeadlineDuringWrite_ResponseThrowsDeadlineE Assert.AreEqual(StatusCode.DeadlineExceeded, ex.Status.StatusCode); } + [Test] + public async Task AsyncUnaryCall_SuccessAndReadValuesAfterDeadline_ValuesReturned() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + httpRequestMessage = request; + + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + invoker.Clock = new TestSystemClock(new DateTime(2019, 11, 29, 1, 1, 1, DateTimeKind.Utc)); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: invoker.Clock.UtcNow.AddSeconds(0.5)), new HelloRequest()); + + // Assert + var result = await call; + Assert.IsNotNull(result); + + // Wait for deadline to trigger + await Task.Delay(1000); + + Assert.IsNotNull(await call.ResponseHeadersAsync); + + Assert.IsNotNull(call.GetTrailers()); + + Assert.AreEqual(StatusCode.OK, call.GetStatus().StatusCode); + } + private class TestSystemClock : ISystemClock { public TestSystemClock(DateTime utcNow) diff --git a/test/Grpc.NetCore.HttpClient.Tests/GetStatusTests.cs b/test/Grpc.NetCore.HttpClient.Tests/GetStatusTests.cs new file mode 100644 index 000000000..5cb0b91f3 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/GetStatusTests.cs @@ -0,0 +1,112 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using NUnit.Framework; +using Greet; +using static Greet.Greeter; +using Grpc.Core; +using Google.Protobuf; +using System.Net.Http; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using System.Net; +using System.Threading.Tasks; +using System; +using System.IO; +using System.Threading; +using System.Net.Http.Headers; +using System.Text; +using System.Linq; +using Grpc.NetCore.HttpClient.Internal; +using Grpc.Tests.Shared; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class GetStatusTests + { + [Test] + public void AsyncUnaryCall_ValidStatusReturned_ReturnsStatus() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: StatusCode.Aborted); + response.TrailingHeaders.Add(GrpcProtocolConstants.MessageTrailer, "value"); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + + // Assert + var ex = Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout()); + Assert.AreEqual(StatusCode.Aborted, ex.StatusCode); + + var status = call.GetStatus(); + Assert.AreEqual(StatusCode.Aborted, status.StatusCode); + Assert.AreEqual("value", status.Detail); + } + + [Test] + public void AsyncUnaryCall_MissingStatus_ThrowError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: null); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + + // Assert + Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout()); + + var ex = Assert.Throws(() => call.GetStatus()); + Assert.AreEqual("Response did not have a grpc-status trailer.", ex.Message); + } + + [Test] + public void AsyncUnaryCall_InvalidStatus_ThrowError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: null); + response.TrailingHeaders.Add(GrpcProtocolConstants.StatusTrailer, "value"); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + + // Assert + Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout()); + + var ex = Assert.Throws(() => call.GetStatus()); + Assert.AreEqual("Unexpected grpc-status value: value", ex.Message); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs b/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs index 8366be377..7bd144e71 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs @@ -270,7 +270,7 @@ public async Task AsyncClientStreamingCall_CompleteWriter_ReturnsTrailers() // Act var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); await call.RequestStream.CompleteAsync().DefaultTimeout(); - await Task.WhenAll(call.ResponseAsync.DefaultTimeout(), trailingHeadersWrittenTcs.Task); + await Task.WhenAll(call.ResponseAsync, trailingHeadersWrittenTcs.Task).DefaultTimeout(); var trailers = call.GetTrailers(); // Assert @@ -297,5 +297,44 @@ public void AsyncClientStreamingCall_UncompleteWriter_ThrowsError() // Assert Assert.AreEqual("Can't get the call trailers because the call is not complete.", ex.Message); } + + [Test] + public void AsyncClientStreamingCall_NotFoundStatus_ThrowsError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var response = ResponseUtils.CreateResponse(HttpStatusCode.NotFound); + return Task.FromResult(response); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + var ex = Assert.Throws(() => call.GetTrailers()); + + // Assert + Assert.AreEqual("Bad gRPC response. Expected HTTP status code 200. Got status code: 404", ex.Message); + } + + [Test] + public void AsyncClientStreamingCall_InvalidContentType_ThrowsError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK); + response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain"); + return Task.FromResult(response); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + var ex = Assert.Throws(() => call.GetTrailers()); + + // Assert + Assert.AreEqual("Bad gRPC response. Invalid content-type value: text/plain", ex.Message); + } } } diff --git a/test/Grpc.NetCore.HttpClient.Tests/HttpContextClientStreamReaderTests.cs b/test/Grpc.NetCore.HttpClient.Tests/HttpContextClientStreamReaderTests.cs index 96e9daf2d..3277edf73 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/HttpContextClientStreamReaderTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/HttpContextClientStreamReaderTests.cs @@ -47,7 +47,7 @@ public void MoveNext_TokenCanceledBeforeCall_ThrowError() }); var call = new GrpcCall(TestHelpers.ServiceMethod, new CallOptions(), SystemClock.Instance); - call.SendServerStreaming(httpClient, new HelloRequest()); + call.StartServerStreaming(httpClient, new HelloRequest()); // Act var moveNextTask1 = call.ClientStreamReader.MoveNext(cts.Token); @@ -72,7 +72,7 @@ public void MoveNext_TokenCanceledDuringCall_ThrowError() }); var call = new GrpcCall(TestHelpers.ServiceMethod, new CallOptions(), SystemClock.Instance); - call.SendServerStreaming(httpClient, new HelloRequest()); + call.StartServerStreaming(httpClient, new HelloRequest()); // Act var moveNextTask1 = call.ClientStreamReader.MoveNext(cts.Token); diff --git a/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/ResponseUtils.cs b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/ResponseUtils.cs index be807256b..87a21705c 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/ResponseUtils.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/ResponseUtils.cs @@ -37,11 +37,10 @@ public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode) => public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, string payload) => CreateResponse(statusCode, new StringContent(payload)); - public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, byte[] payload) => - CreateResponse(statusCode, new ByteArrayContent(payload)); - public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, HttpContent payload, StatusCode? grpcStatusCode = StatusCode.OK) { + payload.Headers.ContentType = GrpcProtocolConstants.GrpcContentTypeHeaderValue; + var message = new HttpResponseMessage(statusCode) { Content = payload diff --git a/test/Grpc.NetCore.HttpClient.Tests/ResponseAsyncTests.cs b/test/Grpc.NetCore.HttpClient.Tests/ResponseAsyncTests.cs index 3d101d85e..0f3fd9dd2 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/ResponseAsyncTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/ResponseAsyncTests.cs @@ -20,6 +20,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Threading.Tasks; using Greet; using Grpc.Core; @@ -85,8 +86,7 @@ public async Task AsyncUnaryCall_DisposeAfterHeadersAndBeforeMessage_ThrowsError call.Dispose(); // Assert - var ex = Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout()); - Assert.AreEqual(StatusCode.Cancelled, ex.StatusCode); + Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout()); var header = responseHeaders.Single(h => h.Key == "custom"); Assert.AreEqual("value!", header.Value); @@ -109,5 +109,44 @@ public void AsyncUnaryCall_ErrorSendingRequest_ThrowsError() // Assert Assert.AreEqual("An error!", ex.Message); } + + [Test] + public void AsyncClientStreamingCall_NotFoundStatus_ThrowsError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var response = ResponseUtils.CreateResponse(HttpStatusCode.NotFound); + return Task.FromResult(response); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + var ex = Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout()); + + // Assert + Assert.AreEqual("Bad gRPC response. Expected HTTP status code 200. Got status code: 404", ex.Message); + } + + [Test] + public void AsyncClientStreamingCall_InvalidContentType_ThrowsError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK); + response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain"); + return Task.FromResult(response); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + var ex = Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout()); + + // Assert + Assert.AreEqual("Bad gRPC response. Invalid content-type value: text/plain", ex.Message); + } } } diff --git a/test/Grpc.NetCore.HttpClient.Tests/ResponseHeadersAsyncTests.cs b/test/Grpc.NetCore.HttpClient.Tests/ResponseHeadersAsyncTests.cs index 2f758d589..4fcb0bb4b 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/ResponseHeadersAsyncTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/ResponseHeadersAsyncTests.cs @@ -180,7 +180,46 @@ public void AsyncServerStreamingCall_DisposeBeforeHeadersReceived_ReturnsError() tcs.TrySetResult(true); // Assert - Assert.CatchAsync(() => call.ResponseHeadersAsync); + Assert.ThrowsAsync(() => call.ResponseHeadersAsync); + } + + [Test] + public void AsyncClientStreamingCall_NotFoundStatus_ThrowsError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var response = ResponseUtils.CreateResponse(HttpStatusCode.NotFound); + return Task.FromResult(response); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + var ex = Assert.ThrowsAsync(async () => await call.ResponseHeadersAsync.DefaultTimeout()); + + // Assert + Assert.AreEqual("Bad gRPC response. Expected HTTP status code 200. Got status code: 404", ex.Message); + } + + [Test] + public void AsyncClientStreamingCall_InvalidContentType_ThrowsError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK); + response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain"); + return Task.FromResult(response); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + var ex = Assert.ThrowsAsync(async () => await call.ResponseHeadersAsync.DefaultTimeout()); + + // Assert + Assert.AreEqual("Bad gRPC response. Invalid content-type value: text/plain", ex.Message); } } } From 524c81557e91f295f9c5ca8af11ae96e642b69dc Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 30 Apr 2019 15:51:46 +1200 Subject: [PATCH 09/13] PR feedback --- .../Internal/GrpcCall.cs | 19 +++++++------ ...er.cs => HttpContentClientStreamReader.cs} | 27 ++++++++++--------- .../Internal/HttpContentClientStreamWriter.cs | 6 ++--- 3 files changed, 26 insertions(+), 26 deletions(-) rename src/Grpc.NetCore.HttpClient/Internal/{HttpContextClientStreamReader.cs => HttpContentClientStreamReader.cs} (80%) diff --git a/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs index 1eeee9a1b..072c01cc2 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs @@ -46,7 +46,7 @@ internal class GrpcCall public Method Method { get; } public Task SendTask { get; private set; } public HttpContentClientStreamWriter ClientStreamWriter { get; private set; } - public HttpContextClientStreamReader ClientStreamReader { get; private set; } + public HttpContentClientStreamReader ClientStreamReader { get; private set; } public GrpcCall(Method method, CallOptions options, ISystemClock clock) { @@ -103,7 +103,7 @@ public void StartServerStreaming(System.Net.Http.HttpClient client, TRequest req var message = CreateHttpRequestMessage(); SetMessageContent(request, message); StartSend(client, message); - ClientStreamReader = new HttpContextClientStreamReader(this); + ClientStreamReader = new HttpContentClientStreamReader(this); } public void StartDuplexStreaming(System.Net.Http.HttpClient client) @@ -111,7 +111,7 @@ public void StartDuplexStreaming(System.Net.Http.HttpClient client) var message = CreateHttpRequestMessage(); ClientStreamWriter = CreateWriter(message); StartSend(client, message); - ClientStreamReader = new HttpContextClientStreamReader(this); + ClientStreamReader = new HttpContentClientStreamReader(this); } public void Dispose() @@ -121,13 +121,14 @@ public void Dispose() Disposed = true; _callCts.Cancel(); - _callCts.Dispose(); _ctsRegistration?.Dispose(); _writerCtsRegistration?.Dispose(); _deadlineTimer?.Dispose(); HttpResponse?.Dispose(); ClientStreamReader?.Dispose(); ClientStreamWriter?.Dispose(); + + _callCts.Dispose(); } } @@ -158,7 +159,7 @@ public Exception CreateCanceledStatusException() return new RpcException(new Status(statusCode, string.Empty)); } - public void FinishResponse(HttpResponseMessage httpResponseMessage) + public void FinishResponse() { if (ResponseFinished) { @@ -172,8 +173,6 @@ public void FinishResponse(HttpResponseMessage httpResponseMessage) // e.g. var reply = await client.SayHelloAsync(new HelloRequest()); Dispose(); - HttpResponse = httpResponseMessage; - var status = GetStatusCore(HttpResponse); if (status.StatusCode != StatusCode.OK) { @@ -213,7 +212,7 @@ public async Task GetResponseAsync() // Trailers are only available once the response body had been read var responseStream = await HttpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); var message = await responseStream.ReadSingleMessageAsync(Method.ResponseMarshaller.Deserializer, _callCts.Token).ConfigureAwait(false); - FinishResponse(HttpResponse); + FinishResponse(); if (message == null) { @@ -314,7 +313,7 @@ private HttpContentClientStreamWriter CreateWriter(HttpRequ message.Content = new PushStreamContent( (stream) => { - writeStreamTcs.SetResult(stream); + writeStreamTcs.TrySetResult(stream); return completeTcs.Task; }, GrpcProtocolConstants.GrpcContentTypeHeaderValue); @@ -355,7 +354,7 @@ private void DeadlineExceeded(object state) { if (!_callCts.IsCancellationRequested) { - // Flag is used to determin status code when generating exceptions + // Flag is used to determine status code when generating exceptions DeadlineReached = true; _callCts.Cancel(); diff --git a/src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs b/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamReader.cs similarity index 80% rename from src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs rename to src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamReader.cs index c7ec45033..a39440ec2 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/HttpContextClientStreamReader.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamReader.cs @@ -25,13 +25,13 @@ namespace Grpc.NetCore.HttpClient.Internal { - internal class HttpContextClientStreamReader : IAsyncStreamReader + internal class HttpContentClientStreamReader : IAsyncStreamReader { private readonly GrpcCall _call; private HttpResponseMessage _httpResponse; private Stream _responseStream; - public HttpContextClientStreamReader(GrpcCall call) + public HttpContentClientStreamReader(GrpcCall call) { _call = call; } @@ -55,10 +55,10 @@ public async Task MoveNext(CancellationToken cancellationToken) throw _call.CreateCanceledStatusException(); } + CancellationTokenSource cts = null; try { // Linking tokens is expensive. Only create a linked token if the token passed in requires it - CancellationTokenSource cts = null; if (cancellationToken.CanBeCanceled) { cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _call.CancellationToken); @@ -81,23 +81,24 @@ public async Task MoveNext(CancellationToken cancellationToken) _responseStream = await _httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); } - using (cts) + Current = await _responseStream.ReadStreamedMessageAsync(_call.Method.ResponseMarshaller.Deserializer, cancellationToken).ConfigureAwait(false); + if (Current == null) { - Current = await _responseStream.ReadStreamedMessageAsync(_call.Method.ResponseMarshaller.Deserializer, cancellationToken).ConfigureAwait(false); - if (Current == null) - { - // No more content in response so mark as finished - _call.FinishResponse(_httpResponse); - return false; - } - - return true; + // No more content in response so mark as finished + _call.FinishResponse(); + return false; } + + return true; } catch (OperationCanceledException) { throw _call.CreateCanceledStatusException(); } + finally + { + cts?.Dispose(); + } } } } diff --git a/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamWriter.cs b/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamWriter.cs index 20e3c8f7b..49847f351 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamWriter.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamWriter.cs @@ -48,7 +48,7 @@ public Task CompleteAsync() lock (_writeLock) { // Pending writes need to be awaited first - if (IsWriteInProgress) + if (IsWriteInProgressUnsynchronized) { return Task.FromException(new InvalidOperationException("Cannot complete client stream writer because the previous write is in progress.")); } @@ -80,7 +80,7 @@ public Task WriteAsync(TRequest message) } // Pending writes need to be awaited first - if (IsWriteInProgress) + if (IsWriteInProgressUnsynchronized) { return Task.FromException(new InvalidOperationException("Cannot write message because the previous write is in progress.")); } @@ -115,7 +115,7 @@ private async Task WriteAsyncCore(TRequest message) /// A value indicating whether there is an async write already in progress. /// Should only check this property when holding the write lock. /// - private bool IsWriteInProgress + private bool IsWriteInProgressUnsynchronized { get { From d24cd7c8c2454b618d53ab5f4cac9acbd443bcb0 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 30 Apr 2019 18:27:13 +1200 Subject: [PATCH 10/13] Throw error when there are multiple MoveNext in progress, add user agent header --- .../Internal/GrpcCall.cs | 13 ++++++ .../Internal/GrpcProtocolConstants.cs | 26 ++++++++++++ .../Internal/HttpContentClientStreamReader.cs | 40 ++++++++++++++++++- .../AsyncUnaryCallTests.cs | 7 ++++ .../DeadlineTests.cs | 30 +++++++++++--- .../HeadersTests.cs | 6 ++- ... => HttpContentClientStreamReaderTests.cs} | 28 ++++++++++++- 7 files changed, 140 insertions(+), 10 deletions(-) rename test/Grpc.NetCore.HttpClient.Tests/{HttpContextClientStreamReaderTests.cs => HttpContentClientStreamReaderTests.cs} (71%) diff --git a/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs index 072c01cc2..caa257138 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs @@ -21,6 +21,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using Grpc.Core; @@ -50,6 +51,9 @@ internal class GrpcCall public GrpcCall(Method method, CallOptions options, ISystemClock clock) { + // Validate deadline before creating any objects that require cleanup + ValidateDeadline(options.Deadline); + _callCts = new CancellationTokenSource(); Method = method; Options = options; @@ -74,6 +78,14 @@ public GrpcCall(Method method, CallOptions options, ISystem } } + private void ValidateDeadline(DateTime? deadline) + { + if (deadline != null && deadline != DateTime.MaxValue && deadline != DateTime.MinValue && deadline.Value.Kind != DateTimeKind.Utc) + { + throw new InvalidOperationException("Deadline must have a kind DateTimeKind.Utc or be equal to DateTime.MaxValue or DateTime.MinValue."); + } + } + public CancellationToken CancellationToken { get { return _callCts.Token; } @@ -326,6 +338,7 @@ private HttpRequestMessage CreateHttpRequestMessage() { var message = new HttpRequestMessage(HttpMethod.Post, Method.FullName); message.Version = new Version(2, 0); + message.Headers.UserAgent.Add(GrpcProtocolConstants.UserAgentHeader); if (Options.Headers != null && Options.Headers.Count > 0) { diff --git a/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolConstants.cs b/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolConstants.cs index d786d2322..0fa4ffcc8 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolConstants.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolConstants.cs @@ -16,7 +16,10 @@ #endregion +using System.Diagnostics; +using System.Linq; using System.Net.Http.Headers; +using System.Reflection; namespace Grpc.NetCore.HttpClient.Internal { @@ -32,5 +35,28 @@ internal static class GrpcProtocolConstants internal const string MessageTrailer = "grpc-message"; internal const string MessageAcceptEncodingHeader = "grpc-accept-encoding"; + + internal static readonly ProductInfoHeaderValue UserAgentHeader; + + static GrpcProtocolConstants() + { + var userAgent = "grpc-dotnet"; + + var assemblyVersion = typeof(GrpcProtocolConstants) + .Assembly + .GetCustomAttributes() + .FirstOrDefault(); + + Debug.Assert(assemblyVersion != null); + + // assembly version attribute should always be present + // but in case it isn't then don't include version in user-agent + if (assemblyVersion != null) + { + userAgent += "/" + assemblyVersion.InformationalVersion; + } + + UserAgentHeader = ProductInfoHeaderValue.Parse(userAgent); + } } } diff --git a/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamReader.cs b/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamReader.cs index a39440ec2..2e1d5b463 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamReader.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamReader.cs @@ -27,13 +27,19 @@ namespace Grpc.NetCore.HttpClient.Internal { internal class HttpContentClientStreamReader : IAsyncStreamReader { + private static readonly Task FinishedTask = Task.FromResult(false); + private readonly GrpcCall _call; + private readonly object _moveNextLock; + private HttpResponseMessage _httpResponse; private Stream _responseStream; + private Task _moveNextTask; public HttpContentClientStreamReader(GrpcCall call) { _call = call; + _moveNextLock = new object(); } public TResponse Current { get; private set; } @@ -42,12 +48,12 @@ public void Dispose() { } - public async Task MoveNext(CancellationToken cancellationToken) + public Task MoveNext(CancellationToken cancellationToken) { // HTTP response has finished if (_call.ResponseFinished) { - return false; + return FinishedTask; } if (_call.IsCancellationRequested) @@ -55,6 +61,23 @@ public async Task MoveNext(CancellationToken cancellationToken) throw _call.CreateCanceledStatusException(); } + lock (_moveNextLock) + { + // Pending move next need to be awaited first + if (IsMoveNextInProgressUnsynchronized) + { + return Task.FromException(new InvalidOperationException("Cannot read next message because the previous read is in progress.")); + } + + // Save move next task to track whether it is complete + _moveNextTask = MoveNextCore(cancellationToken); + } + + return _moveNextTask; + } + + private async Task MoveNextCore(CancellationToken cancellationToken) + { CancellationTokenSource cts = null; try { @@ -100,5 +123,18 @@ public async Task MoveNext(CancellationToken cancellationToken) cts?.Dispose(); } } + + /// + /// A value indicating whether there is an async move next already in progress. + /// Should only check this property when holding the move next lock. + /// + private bool IsMoveNextInProgressUnsynchronized + { + get + { + var moveNextTask = _moveNextTask; + return moveNextTask != null && !moveNextTask.IsCompleted; + } + } } } diff --git a/test/Grpc.NetCore.HttpClient.Tests/AsyncUnaryCallTests.cs b/test/Grpc.NetCore.HttpClient.Tests/AsyncUnaryCallTests.cs index bfe7bd375..079bdb5f2 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/AsyncUnaryCallTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/AsyncUnaryCallTests.cs @@ -17,6 +17,7 @@ #endregion using System; +using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -24,6 +25,7 @@ using System.Threading.Tasks; using Greet; using Grpc.Core; +using Grpc.NetCore.HttpClient.Internal; using Grpc.NetCore.HttpClient.Tests.Infrastructure; using Grpc.Tests.Shared; using NUnit.Framework; @@ -65,6 +67,11 @@ public async Task AsyncUnaryCall_Success_HttpRequestMessagePopulated() Assert.AreEqual(HttpMethod.Post, httpRequestMessage.Method); Assert.AreEqual(new Uri("https://localhost/ServiceName/MethodName"), httpRequestMessage.RequestUri); Assert.AreEqual(new MediaTypeHeaderValue("application/grpc"), httpRequestMessage.Content.Headers.ContentType); + + var userAgent = httpRequestMessage.Headers.UserAgent.Single(); + Assert.AreEqual(GrpcProtocolConstants.UserAgentHeader, userAgent); + Assert.AreEqual("grpc-dotnet", userAgent.Product.Name); + Assert.IsTrue(!string.IsNullOrEmpty(userAgent.Product.Version)); } [Test] diff --git a/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs b/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs index a4c5ebae6..d0e651c16 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs @@ -57,8 +57,7 @@ public async Task AsyncUnaryCall_SetSecondDeadline_RequestMessageContainsDeadlin // Assert Assert.IsNotNull(httpRequestMessage); - Assert.AreEqual(1, httpRequestMessage.Headers.Count()); - Assert.AreEqual("1S", httpRequestMessage.Headers.GetValues("grpc-timeout").Single()); + Assert.AreEqual("1S", httpRequestMessage.Headers.GetValues(GrpcProtocolConstants.TimeoutHeader).Single()); } [Test] @@ -81,7 +80,7 @@ public async Task AsyncUnaryCall_SetMaxValueDeadline_RequestMessageHasNoDeadline // Assert Assert.IsNotNull(httpRequestMessage); - Assert.AreEqual(0, httpRequestMessage.Headers.Count()); + Assert.AreEqual(0, httpRequestMessage.Headers.Count(h => string.Equals(h.Key, GrpcProtocolConstants.TimeoutHeader, StringComparison.OrdinalIgnoreCase))); } [Test] @@ -116,8 +115,7 @@ public async Task AsyncUnaryCall_SendDeadlineHeaderAndDeadlineValue_DeadlineValu Assert.AreEqual("Hello world", rs.Message); Assert.IsNotNull(httpRequestMessage); - Assert.AreEqual(1, httpRequestMessage.Headers.Count()); - Assert.AreEqual("1S", httpRequestMessage.Headers.GetValues("grpc-timeout").Single()); + Assert.AreEqual("1S", httpRequestMessage.Headers.GetValues(GrpcProtocolConstants.TimeoutHeader).Single()); } [Test] @@ -237,6 +235,28 @@ public async Task AsyncUnaryCall_SuccessAndReadValuesAfterDeadline_ValuesReturne Assert.AreEqual(StatusCode.OK, call.GetStatus().StatusCode); } + [Test] + public void AsyncUnaryCall_SetNonUtcDeadline_ThrowError() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + httpRequestMessage = request; + + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var ex = Assert.ThrowsAsync(async () => await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: new DateTime(2000, DateTimeKind.Local)), new HelloRequest())); + + // Assert + Assert.AreEqual("Deadline must have a kind DateTimeKind.Utc or be equal to DateTime.MaxValue or DateTime.MinValue.", ex.Message); + } + private class TestSystemClock : ISystemClock { public TestSystemClock(DateTime utcNow) diff --git a/test/Grpc.NetCore.HttpClient.Tests/HeadersTests.cs b/test/Grpc.NetCore.HttpClient.Tests/HeadersTests.cs index 0e39913dc..651f62675 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/HeadersTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/HeadersTests.cs @@ -28,6 +28,7 @@ using Grpc.Core; using Grpc.NetCore.HttpClient.Tests.Infrastructure; using Grpc.Tests.Shared; +using Microsoft.Net.Http.Headers; using NUnit.Framework; namespace Grpc.NetCore.HttpClient.Tests @@ -67,7 +68,6 @@ public async Task AsyncUnaryCall_SendHeaders_RequestMessageContainsHeaders() Assert.AreEqual("Hello world", rs.Message); Assert.IsNotNull(httpRequestMessage); - Assert.AreEqual(2, httpRequestMessage.Headers.Count()); Assert.AreEqual("ascii", httpRequestMessage.Headers.GetValues("custom").Single()); Assert.AreEqual("Hello world", Encoding.UTF8.GetString(Convert.FromBase64String(httpRequestMessage.Headers.GetValues("custom-bin").Single()))); } @@ -102,7 +102,9 @@ public async Task AsyncUnaryCall_NoHeaders_RequestMessageHasNoHeaders() Assert.AreEqual("Hello world", rs.Message); Assert.IsNotNull(httpRequestMessage); - Assert.AreEqual(0, httpRequestMessage.Headers.Count()); + + // User-Agent is always sent + Assert.AreEqual(0, httpRequestMessage.Headers.Count(h => !string.Equals(h.Key, HeaderNames.UserAgent, StringComparison.OrdinalIgnoreCase))); } } } diff --git a/test/Grpc.NetCore.HttpClient.Tests/HttpContextClientStreamReaderTests.cs b/test/Grpc.NetCore.HttpClient.Tests/HttpContentClientStreamReaderTests.cs similarity index 71% rename from test/Grpc.NetCore.HttpClient.Tests/HttpContextClientStreamReaderTests.cs rename to test/Grpc.NetCore.HttpClient.Tests/HttpContentClientStreamReaderTests.cs index 3277edf73..d7579454a 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/HttpContextClientStreamReaderTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/HttpContentClientStreamReaderTests.cs @@ -16,6 +16,7 @@ #endregion +using System; using System.Net; using System.Net.Http; using System.Threading; @@ -30,7 +31,7 @@ namespace Grpc.NetCore.HttpClient.Tests { [TestFixture] - public class HttpContextClientStreamReaderTests + public class HttpContentClientStreamReaderTests { [Test] public void MoveNext_TokenCanceledBeforeCall_ThrowError() @@ -85,5 +86,30 @@ public void MoveNext_TokenCanceledDuringCall_ThrowError() var ex = Assert.ThrowsAsync(async () => await moveNextTask1.DefaultTimeout()); Assert.AreEqual(StatusCode.Cancelled, ex.StatusCode); } + + [Test] + public void MoveNext_MultipleCallsWithoutAwait_ThrowError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var stream = new SyncPointMemoryStream(); + var content = new StreamContent(stream); + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, content)); + }); + + var call = new GrpcCall(TestHelpers.ServiceMethod, new CallOptions(), SystemClock.Instance); + call.StartServerStreaming(httpClient, new HelloRequest()); + + // Act + var moveNextTask1 = call.ClientStreamReader.MoveNext(CancellationToken.None); + var moveNextTask2 = call.ClientStreamReader.MoveNext(CancellationToken.None); + + // Assert + Assert.IsFalse(moveNextTask1.IsCompleted); + + var ex = Assert.ThrowsAsync(async () => await moveNextTask2.DefaultTimeout()); + Assert.AreEqual("Cannot read next message because the previous read is in progress.", ex.Message); + } } } From 7a140ce5089cfd8bdef0e15b79f2b2cc5e74256f Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 1 May 2019 07:26:57 +1200 Subject: [PATCH 11/13] PR feedback --- src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs index caa257138..ee0c2aa21 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs @@ -21,14 +21,13 @@ using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using Grpc.Core; namespace Grpc.NetCore.HttpClient.Internal { - internal class GrpcCall + internal class GrpcCall : IDisposable { private readonly CancellationTokenSource _callCts; private readonly CancellationTokenRegistration? _ctsRegistration; @@ -180,12 +179,14 @@ public void FinishResponse() ResponseFinished = true; + // Get status from response before dispose + var status = GetStatusCore(HttpResponse); + // Clean up call resources once this call is finished // Call may not be explicitly disposed when used with unary methods // e.g. var reply = await client.SayHelloAsync(new HelloRequest()); Dispose(); - var status = GetStatusCore(HttpResponse); if (status.StatusCode != StatusCode.OK) { throw new RpcException(status); From dd1b2342b5205288c1e7720cf97a6832ab674ccc Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 2 May 2019 12:50:29 +1200 Subject: [PATCH 12/13] PR feedback --- .../Internal/GrpcCall.cs | 100 +++++++++++++----- .../GetStatusTests.cs | 47 ++++++++ testassets/InteropTestsClient/IChannel.cs | 21 +++- 3 files changed, 141 insertions(+), 27 deletions(-) diff --git a/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs index ee0c2aa21..db76ef9ba 100644 --- a/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs +++ b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs @@ -21,6 +21,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using Grpc.Core; @@ -125,13 +126,30 @@ public void StartDuplexStreaming(System.Net.Http.HttpClient client) ClientStreamReader = new HttpContentClientStreamReader(this); } + /// + /// Dispose can be called by: + /// 1. The user. AsyncUnaryCall.Dispose et al will call this Dispose + /// 2. will call dispose if errors fail validation + /// 3. will call dispose + /// public void Dispose() { if (!Disposed) { Disposed = true; - _callCts.Cancel(); + if (!ResponseFinished) + { + // If the response is not finished then cancel any pending actions: + // 1. Call HttpClient.SendAsync + // 2. Response Stream.ReadAsync + // 3. Client stream + // - Getting the Stream from the Request.HttpContent + // - Holding the Request.HttpContent.SerializeToStream open + // - Writing to the client stream + _callCts.Cancel(); + } + _ctsRegistration?.Dispose(); _writerCtsRegistration?.Dispose(); _deadlineTimer?.Dispose(); @@ -139,7 +157,10 @@ public void Dispose() ClientStreamReader?.Dispose(); ClientStreamWriter?.Dispose(); - _callCts.Dispose(); + // To avoid racing with Dispose, skip disposing the call CTS + // This avoid Dispose potentially calling cancel on a disposed CTS + // The call CTS is not exposed externally and all dependent registrations + // are cleaned up } } @@ -170,26 +191,33 @@ public Exception CreateCanceledStatusException() return new RpcException(new Status(statusCode, string.Empty)); } + /// + /// Marks the response as finished, i.e. all response content has been read and trailers are available. + /// Can be called by for unary and client streaming calls, or + /// + /// for server streaming and duplex streaming calls. + /// public void FinishResponse() { - if (ResponseFinished) - { - return; - } - ResponseFinished = true; - // Get status from response before dispose - var status = GetStatusCore(HttpResponse); - - // Clean up call resources once this call is finished - // Call may not be explicitly disposed when used with unary methods - // e.g. var reply = await client.SayHelloAsync(new HelloRequest()); - Dispose(); + try + { + // Get status from response before dispose + // This may throw an error if the grpc-status is missing or malformed + var status = GetStatusCore(HttpResponse); - if (status.StatusCode != StatusCode.OK) + if (status.StatusCode != StatusCode.OK) + { + throw new RpcException(status); + } + } + finally { - throw new RpcException(status); + // Clean up call resources once this call is finished + // Call may not be explicitly disposed when used with unary methods + // e.g. var reply = await client.SayHelloAsync(new HelloRequest()); + Dispose(); } } @@ -366,7 +394,9 @@ private HttpRequestMessage CreateHttpRequestMessage() private void DeadlineExceeded(object state) { - if (!_callCts.IsCancellationRequested) + // Deadline is only exceeded if the timeout has passed and + // the response has not been finished or canceled + if (!_callCts.IsCancellationRequested && !ResponseFinished) { // Flag is used to determine status code when generating exceptions DeadlineReached = true; @@ -377,10 +407,9 @@ private void DeadlineExceeded(object state) private static Status GetStatusCore(HttpResponseMessage httpResponseMessage) { + string grpcStatus = GetHeaderValue(httpResponseMessage.TrailingHeaders, GrpcProtocolConstants.StatusTrailer); // grpc-status is a required trailer - string grpcStatus; - if (!httpResponseMessage.TrailingHeaders.TryGetValues(GrpcProtocolConstants.StatusTrailer, out var grpcStatusValues) || - (grpcStatus = grpcStatusValues.FirstOrDefault()) == null) + if (grpcStatus == null) { throw new InvalidOperationException("Response did not have a grpc-status trailer."); } @@ -392,16 +421,39 @@ private static Status GetStatusCore(HttpResponseMessage httpResponseMessage) } // grpc-message is optional - string grpcMessage = null; - if (httpResponseMessage.TrailingHeaders.TryGetValues(GrpcProtocolConstants.MessageTrailer, out var grpcMessageValues)) + string grpcMessage = GetHeaderValue(httpResponseMessage.TrailingHeaders, GrpcProtocolConstants.MessageTrailer); + if (!string.IsNullOrEmpty(grpcMessage)) { - // TODO(JamesNK): Unescape percent encoding - grpcMessage = grpcMessageValues.FirstOrDefault(); + // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses + // The value portion of Status-Message is conceptually a Unicode string description of the error, + // physically encoded as UTF-8 followed by percent-encoding. + grpcMessage = Uri.UnescapeDataString(grpcMessage); } return new Status((StatusCode)statusValue, grpcMessage); } + private static string GetHeaderValue(HttpHeaders headers, string name) + { + if (!headers.TryGetValues(name, out var values)) + { + return null; + } + + // HttpHeaders appears to always return an array, but fallback to converting values to one just in case + var valuesArray = values as string[] ?? values.ToArray(); + + switch (valuesArray.Length) + { + case 0: + return null; + case 1: + return valuesArray[0]; + default: + throw new InvalidOperationException($"Multiple {name} headers."); + } + } + private void ValidateTrailersAvailable() { // Response headers have been returned and are not a valid grpc response diff --git a/test/Grpc.NetCore.HttpClient.Tests/GetStatusTests.cs b/test/Grpc.NetCore.HttpClient.Tests/GetStatusTests.cs index 5cb0b91f3..ad64cfd23 100644 --- a/test/Grpc.NetCore.HttpClient.Tests/GetStatusTests.cs +++ b/test/Grpc.NetCore.HttpClient.Tests/GetStatusTests.cs @@ -64,6 +64,53 @@ public void AsyncUnaryCall_ValidStatusReturned_ReturnsStatus() Assert.AreEqual("value", status.Detail); } + [Test] + public void AsyncUnaryCall_PercentEncodedMessage_MessageDecoded() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: StatusCode.Aborted); + response.TrailingHeaders.Add(GrpcProtocolConstants.MessageTrailer, "%C2%A3"); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + + // Assert + var ex = Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout()); + Assert.AreEqual(StatusCode.Aborted, ex.StatusCode); + + var status = call.GetStatus(); + Assert.AreEqual(StatusCode.Aborted, status.StatusCode); + Assert.AreEqual("£", status.Detail); + } + + [Test] + public void AsyncUnaryCall_MultipleStatusHeaders_ThrowError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: StatusCode.Aborted); + response.TrailingHeaders.Add(GrpcProtocolConstants.MessageTrailer, "one"); + response.TrailingHeaders.Add(GrpcProtocolConstants.MessageTrailer, "two"); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + + // Assert + var ex = Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout()); + Assert.AreEqual("Multiple grpc-message headers.", ex.Message); + } + [Test] public void AsyncUnaryCall_MissingStatus_ThrowError() { diff --git a/testassets/InteropTestsClient/IChannel.cs b/testassets/InteropTestsClient/IChannel.cs index 9e5611e44..f0a1510d1 100644 --- a/testassets/InteropTestsClient/IChannel.cs +++ b/testassets/InteropTestsClient/IChannel.cs @@ -1,6 +1,21 @@ -using System; -using System.Collections.Generic; -using System.Text; +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + using System.Threading.Tasks; using Grpc.Core; From 5a9de799780fe3ff87447b739938f9e7ca431e8f Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 2 May 2019 13:50:56 +1200 Subject: [PATCH 13/13] Interop project clean up --- .../GrpcClientFactory.cs | 28 ++++++++++++--- testassets/InteropTestsClient/IChannel.cs | 10 ++++++ .../InteropTestsClient/InteropClient.cs | 35 +++++++++++++++---- testassets/InteropTestsClient/RunTests.ps1 | 2 +- 4 files changed, 64 insertions(+), 11 deletions(-) diff --git a/src/Grpc.NetCore.HttpClient/GrpcClientFactory.cs b/src/Grpc.NetCore.HttpClient/GrpcClientFactory.cs index 3e8d36144..cca471861 100644 --- a/src/Grpc.NetCore.HttpClient/GrpcClientFactory.cs +++ b/src/Grpc.NetCore.HttpClient/GrpcClientFactory.cs @@ -42,14 +42,34 @@ public static TClient Create(string baseAddress, X509Certificate certif // Needs to be set before creating the HttpClientHandler AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2Support", true); - var handler = new HttpClientHandler(); - handler.ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) => true; + var httpClientHandler = new HttpClientHandler(); + httpClientHandler.ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) => true; if (certificate != null) { - handler.ClientCertificates.Add(certificate); + httpClientHandler.ClientCertificates.Add(certificate); } - var httpClient = new System.Net.Http.HttpClient(handler); + return CreateCore(baseAddress, httpClientHandler); + } + + /// + /// Creates a gRPC client using the specified address and handler. + /// + /// The type of the gRPC client. This type will typically be defined using generated code from a *.proto file. + /// The base address to use when making gRPC requests. + /// The . + /// A gRPC client. + public static TClient Create(string baseAddress, HttpClientHandler httpClientHandler) where TClient : ClientBase + { + return CreateCore(baseAddress, httpClientHandler); + } + + private static TClient CreateCore(string baseAddress, HttpClientHandler httpClientHandler) where TClient : ClientBase + { + // Needs to be set before creating the HttpClientHandler + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2Support", true); + + var httpClient = new System.Net.Http.HttpClient(httpClientHandler); httpClient.BaseAddress = new Uri(baseAddress, UriKind.RelativeOrAbsolute); return Cache.Instance.Activator(new HttpClientCallInvoker(httpClient)); diff --git a/testassets/InteropTestsClient/IChannel.cs b/testassets/InteropTestsClient/IChannel.cs index f0a1510d1..47a2c949b 100644 --- a/testassets/InteropTestsClient/IChannel.cs +++ b/testassets/InteropTestsClient/IChannel.cs @@ -16,6 +16,8 @@ #endregion +using System; +using System.Net.Http; using System.Threading.Tasks; using Grpc.Core; @@ -28,8 +30,16 @@ public interface IChannel public class HttpClientChannel : IChannel { + public HttpClientHandler HttpClientHandler { get; } + + public HttpClientChannel(HttpClientHandler httpClient) + { + HttpClientHandler = httpClient; + } + public Task ShutdownAsync() { + HttpClientHandler.Dispose(); return Task.CompletedTask; } } diff --git a/testassets/InteropTestsClient/InteropClient.cs b/testassets/InteropTestsClient/InteropClient.cs index 42b73ead2..2702f1960 100644 --- a/testassets/InteropTestsClient/InteropClient.cs +++ b/testassets/InteropTestsClient/InteropClient.cs @@ -21,6 +21,7 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using CommandLine; @@ -43,7 +44,11 @@ public class InteropClient private class ClientOptions { - [Option("client_type", Default = "httpclient")] + [Option("client_type" +#if DEBUG + , Default = "httpclient" +#endif + )] public string ClientType { get; set; } [Option("server_host", Default = "localhost")] @@ -52,10 +57,18 @@ private class ClientOptions [Option("server_host_override")] public string ServerHostOverride { get; set; } - [Option("server_port", Default = 50052)] + [Option("server_port" +#if DEBUG + , Default = 50052 +#endif + )] public int ServerPort { get; set; } - [Option("test_case", Default = "timeout_on_sleeping_server")] + [Option("test_case" +#if DEBUG + , Default = "large_unary" +#endif + )] public string TestCase { get; set; } // Deliberately using nullable bool type to allow --use_tls=true syntax (as opposed to --use_tls) @@ -108,7 +121,13 @@ private async Task Run() private Task HttpClientCreateChannel() { - return Task.FromResult(new HttpClientChannel()); + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2Support", true); + + var httpClientHandler = new HttpClientHandler(); + httpClientHandler.ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) => true; + + return Task.FromResult(new HttpClientChannel(httpClientHandler)); } private async Task CoreCreateChannel() @@ -124,6 +143,7 @@ private async Task CoreCreateChannel() }; } var channel = new Channel(options.ServerHost, options.ServerPort, credentials, channelOptions); + await channel.ConnectAsync(); return new CoreChannel(channel); } @@ -159,10 +179,13 @@ private TClient CreateClient(IChannel channel) where TClient : ClientBa { return (TClient)Activator.CreateInstance(typeof(TClient), coreChannel.Channel); } + else if (channel is HttpClientChannel httpClientChannel) + { + return GrpcClientFactory.Create($"http://{options.ServerHost}:{options.ServerPort}", certificate: null); + } else { - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - return GrpcClientFactory.Create($"http://{options.ServerHost}:{options.ServerPort}"); + throw new Exception("Unexpected channel type."); } } diff --git a/testassets/InteropTestsClient/RunTests.ps1 b/testassets/InteropTestsClient/RunTests.ps1 index c2093dced..9ef6fd741 100644 --- a/testassets/InteropTestsClient/RunTests.ps1 +++ b/testassets/InteropTestsClient/RunTests.ps1 @@ -28,7 +28,7 @@ Write-Host foreach ($test in $allTests) { Write-Host "Running $test" -ForegroundColor Cyan - dotnet run --use_tls true --server_port 50052 --client_type HttpClient --test_case $test + dotnet run --use_tls false --server_port 50052 --client_type httpclient --test_case $test Write-Host }