Skip to content

Commit ec6de5c

Browse files
authored
Improve error message when CORS is not configured correctly (#1171)
1 parent e34cef6 commit ec6de5c

File tree

7 files changed

+88
-6
lines changed

7 files changed

+88
-6
lines changed

src/Grpc.Net.Client/GrpcChannel.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@ public sealed class GrpcChannel : ChannelBase, IDisposable
5555
internal Dictionary<string, ICompressionProvider> CompressionProviders { get; }
5656
internal string MessageAcceptEncoding { get; }
5757
internal bool Disposed { get; private set; }
58-
// Timing related options that are set in unit tests
58+
59+
// Options that are set in unit tests
5960
internal ISystemClock Clock = SystemClock.Instance;
61+
internal IOperatingSystem OperatingSystem = Internal.OperatingSystem.Instance;
6062
internal bool DisableClientDeadline;
6163
internal long MaxTimerDueTime = uint.MaxValue - 1; // Max System.Threading.Timer due time
6264

src/Grpc.Net.Client/Internal/GrpcCall.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ private async Task RunCall(HttpRequestMessage request, TimeSpan? timeout)
503503
GrpcProtocolHelpers.GetGrpcEncoding(HttpResponse),
504504
singleMessage: true,
505505
_callCts.Token).ConfigureAwait(false);
506-
status = GrpcProtocolHelpers.GetResponseStatus(HttpResponse);
506+
status = GrpcProtocolHelpers.GetResponseStatus(HttpResponse, Channel.OperatingSystem.IsBrowser);
507507

508508
if (message == null)
509509
{

src/Grpc.Net.Client/Internal/GrpcProtocolHelpers.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,14 +327,20 @@ public static void AddHeader(HttpRequestHeaders headers, Metadata.Entry entry)
327327
}
328328
}
329329

330-
public static Status GetResponseStatus(HttpResponseMessage httpResponse)
330+
public static Status GetResponseStatus(HttpResponseMessage httpResponse, bool isBrowser)
331331
{
332332
Status? status;
333333
try
334334
{
335335
if (!TryGetStatusCore(httpResponse.TrailingHeaders, out status))
336336
{
337-
status = new Status(StatusCode.Cancelled, "No grpc-status found on response.");
337+
var detail = "No grpc-status found on response.";
338+
if (isBrowser)
339+
{
340+
detail += " If the gRPC call is cross domain then CORS must be correctly configured. Access-Control-Expose-Headers needs to include 'grpc-status' and 'grpc-message'.";
341+
}
342+
343+
status = new Status(StatusCode.Cancelled, detail);
338344
}
339345
}
340346
catch (Exception ex)

src/Grpc.Net.Client/Internal/HttpContentClientStreamReader.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ private async Task<bool> MoveNextCore(CancellationToken cancellationToken)
167167
{
168168
// No more content in response so report status to call.
169169
// The call will handle finishing the response.
170-
var status = GrpcProtocolHelpers.GetResponseStatus(_httpResponse);
170+
var status = GrpcProtocolHelpers.GetResponseStatus(_httpResponse, _call.Channel.OperatingSystem.IsBrowser);
171171
_call.ResponseStreamEnded(status);
172172
if (status.StatusCode != StatusCode.OK)
173173
{
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#region Copyright notice and license
2+
3+
// Copyright 2019 The gRPC Authors
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
#endregion
18+
19+
using System.Runtime.InteropServices;
20+
21+
namespace Grpc.Net.Client.Internal
22+
{
23+
internal interface IOperatingSystem
24+
{
25+
bool IsBrowser { get; }
26+
}
27+
28+
internal class OperatingSystem : IOperatingSystem
29+
{
30+
public static readonly OperatingSystem Instance = new OperatingSystem();
31+
32+
public bool IsBrowser { get; }
33+
34+
private OperatingSystem()
35+
{
36+
IsBrowser = RuntimeInformation.IsOSPlatform(OSPlatform.Create("browser"));
37+
}
38+
}
39+
}

test/Grpc.Net.Client.Tests/GetStatusTests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,36 @@ public async Task AsyncUnaryCall_MissingStatus_ThrowError()
187187
Assert.AreEqual(StatusCode.Cancelled, call.GetStatus().StatusCode);
188188
}
189189

190+
[Test]
191+
public async Task AsyncUnaryCall_MissingStatusBrowser_ThrowError()
192+
{
193+
// Arrange
194+
var httpClient = ClientTestHelpers.CreateTestClient(async request =>
195+
{
196+
var streamContent = await ClientTestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout();
197+
var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: null);
198+
return response;
199+
});
200+
201+
var os = new TestOperatingSystem { IsBrowser = true };
202+
var invoker = HttpClientCallInvokerFactory.Create(httpClient, operatingSystem: os);
203+
204+
// Act
205+
var call = invoker.AsyncUnaryCall<HelloRequest, HelloReply>(ClientTestHelpers.ServiceMethod, string.Empty, new CallOptions(), new HelloRequest());
206+
207+
// Assert
208+
var ex = await ExceptionAssert.ThrowsAsync<RpcException>(() => call.ResponseAsync).DefaultTimeout();
209+
210+
Assert.AreEqual("No grpc-status found on response. If the gRPC call is cross domain then CORS must be correctly configured. Access-Control-Expose-Headers needs to include 'grpc-status' and 'grpc-message'.", ex.Status.Detail);
211+
Assert.AreEqual(StatusCode.Cancelled, ex.StatusCode);
212+
Assert.AreEqual(StatusCode.Cancelled, call.GetStatus().StatusCode);
213+
}
214+
215+
private class TestOperatingSystem : IOperatingSystem
216+
{
217+
public bool IsBrowser { get; set; }
218+
}
219+
190220
[Test]
191221
public async Task AsyncUnaryCall_InvalidStatus_ThrowError()
192222
{

test/Grpc.Net.Client.Tests/Infrastructure/HttpClientCallInvokerFactory.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ public static HttpClientCallInvoker Create(
3131
ISystemClock? systemClock = null,
3232
Action<GrpcChannelOptions>? configure = null,
3333
bool? disableClientDeadline = null,
34-
long? maxTimerPeriod = null)
34+
long? maxTimerPeriod = null,
35+
IOperatingSystem? operatingSystem = null)
3536
{
3637
var channelOptions = new GrpcChannelOptions
3738
{
@@ -50,6 +51,10 @@ public static HttpClientCallInvoker Create(
5051
{
5152
channel.MaxTimerDueTime = maxTimerPeriod.Value;
5253
}
54+
if (operatingSystem != null)
55+
{
56+
channel.OperatingSystem = operatingSystem;
57+
}
5358

5459
return new HttpClientCallInvoker(channel);
5560
}

0 commit comments

Comments
 (0)