Skip to content

Commit 3fb0862

Browse files
authored
Map HttpStatusCode to gRPC status code (#575)
1 parent 1f67ee2 commit 3fb0862

File tree

5 files changed

+109
-13
lines changed

5 files changed

+109
-13
lines changed

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

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -237,9 +237,17 @@ public Task<TResponse> GetResponseAsync()
237237
{
238238
Log.ResponseHeadersReceived(Logger);
239239

240+
// gRPC status can be returned in the header when there is no message (e.g. unimplemented status)
241+
// An explicitly specified status header has priority over other failing statuses
242+
if (GrpcProtocolHelpers.TryGetStatusCore(httpResponse.Headers, out var status))
243+
{
244+
return status;
245+
}
246+
240247
if (httpResponse.StatusCode != HttpStatusCode.OK)
241248
{
242-
return new Status(StatusCode.Cancelled, "Bad gRPC response. Expected HTTP status code 200. Got status code: " + (int)httpResponse.StatusCode);
249+
var statusCode = MapHttpStatusToGrpcCode(httpResponse.StatusCode);
250+
return new Status(statusCode, "Bad gRPC response. HTTP status code: " + (int)httpResponse.StatusCode);
243251
}
244252

245253
if (httpResponse.Content?.Headers.ContentType == null)
@@ -252,19 +260,40 @@ public Task<TResponse> GetResponseAsync()
252260
{
253261
return new Status(StatusCode.Cancelled, "Bad gRPC response. Invalid content-type value: " + grpcEncoding);
254262
}
255-
else
256-
{
257-
// gRPC status can be returned in the header when there is no message (e.g. unimplemented status)
258-
if (GrpcProtocolHelpers.TryGetStatusCore(httpResponse.Headers, out var status))
259-
{
260-
return status;
261-
}
262-
}
263263

264264
// Call is still in progress
265265
return null;
266266
}
267267

268+
private static StatusCode MapHttpStatusToGrpcCode(HttpStatusCode httpStatusCode)
269+
{
270+
switch (httpStatusCode)
271+
{
272+
case HttpStatusCode.BadRequest: // 400
273+
case HttpStatusCode.RequestHeaderFieldsTooLarge: // 431
274+
return StatusCode.Internal;
275+
case HttpStatusCode.Unauthorized: // 401
276+
return StatusCode.Unauthenticated;
277+
case HttpStatusCode.Forbidden: // 403
278+
return StatusCode.PermissionDenied;
279+
case HttpStatusCode.NotFound: // 404
280+
return StatusCode.Unimplemented;
281+
case HttpStatusCode.TooManyRequests: // 429
282+
case HttpStatusCode.BadGateway: // 502
283+
case HttpStatusCode.ServiceUnavailable: // 503
284+
case HttpStatusCode.GatewayTimeout: // 504
285+
return StatusCode.Unavailable;
286+
default:
287+
if ((int)httpStatusCode >= 100 && (int)httpStatusCode < 200)
288+
{
289+
// 1xx. These headers should have been ignored.
290+
return StatusCode.Internal;
291+
}
292+
293+
return StatusCode.Unknown;
294+
}
295+
}
296+
268297
public Metadata GetTrailers()
269298
{
270299
using (StartScope())

test/FunctionalTests/Client/CancellationTests.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,15 @@ async Task ServerStreamingCall(DataMessage request, IServerStreamWriter<DataMess
228228
return true;
229229
}
230230

231+
// Cancellation happened after checking token but before writing message
232+
if (writeContext.LoggerName == "Grpc.AspNetCore.Server.ServerCallHandler" &&
233+
writeContext.EventId.Name == "ErrorExecutingServiceMethod" &&
234+
writeContext.Exception is InvalidOperationException &&
235+
writeContext.Exception.Message == "Cannot write message after request is complete.")
236+
{
237+
return true;
238+
}
239+
231240
if (writeContext.LoggerName == "Grpc.Net.Client.Internal.GrpcCall" &&
232241
writeContext.EventId.Name == "GrpcStatusError" &&
233242
writeContext.Message == "Call failed with gRPC error status. Status code: 'Cancelled', Message: 'Call canceled by the client.'.")

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,8 @@ public async Task ClientStreamReader_WriteWithInvalidHttpStatus_ErrorThrown()
166166
// Assert
167167
var ex = await ExceptionAssert.ThrowsAsync<RpcException>(() => call.ResponseStream.MoveNext(CancellationToken.None)).DefaultTimeout();
168168

169-
Assert.AreEqual(StatusCode.Cancelled, ex.StatusCode);
170-
Assert.AreEqual("Bad gRPC response. Expected HTTP status code 200. Got status code: 404", ex.Status.Detail);
169+
Assert.AreEqual(StatusCode.Unimplemented, ex.StatusCode);
170+
Assert.AreEqual("Bad gRPC response. HTTP status code: 404", ex.Status.Detail);
171171
}
172172

173173
[Test]

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

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
using System;
2020
using System.Net;
21+
using System.Net.Http;
2122
using System.Threading.Tasks;
2223
using Greet;
2324
using Grpc.Core;
@@ -31,6 +32,63 @@ namespace Grpc.Net.Client.Tests
3132
[TestFixture]
3233
public class GetStatusTests
3334
{
35+
[TestCase(HttpStatusCode.BadRequest, StatusCode.Internal)]
36+
[TestCase(HttpStatusCode.RequestHeaderFieldsTooLarge, StatusCode.Internal)]
37+
[TestCase(HttpStatusCode.Unauthorized, StatusCode.Unauthenticated)]
38+
[TestCase(HttpStatusCode.Forbidden, StatusCode.PermissionDenied)]
39+
[TestCase(HttpStatusCode.NotFound, StatusCode.Unimplemented)]
40+
[TestCase(HttpStatusCode.TooManyRequests, StatusCode.Unavailable)]
41+
[TestCase(HttpStatusCode.BadGateway, StatusCode.Unavailable)]
42+
[TestCase(HttpStatusCode.ServiceUnavailable, StatusCode.Unavailable)]
43+
[TestCase(HttpStatusCode.GatewayTimeout, StatusCode.Unavailable)]
44+
[TestCase(HttpStatusCode.Continue, StatusCode.Internal)]
45+
[TestCase(HttpStatusCode.UpgradeRequired, StatusCode.Unknown)]
46+
public async Task AsyncUnaryCall_Non200HttpStatusCode_MappedToGrpcStatusCode(HttpStatusCode httpStatusCode, StatusCode statusCode)
47+
{
48+
// Arrange
49+
var httpClient = ClientTestHelpers.CreateTestClient(request =>
50+
{
51+
var response = new HttpResponseMessage { StatusCode = httpStatusCode };
52+
return Task.FromResult(response);
53+
});
54+
var invoker = HttpClientCallInvokerFactory.Create(httpClient);
55+
56+
// Act
57+
var call = invoker.AsyncUnaryCall<HelloRequest, HelloReply>(ClientTestHelpers.ServiceMethod, string.Empty, new CallOptions(), new HelloRequest());
58+
59+
// Assert
60+
var ex = await ExceptionAssert.ThrowsAsync<RpcException>(() => call.ResponseAsync).DefaultTimeout();
61+
Assert.AreEqual(statusCode, ex.StatusCode);
62+
63+
var status = call.GetStatus();
64+
Assert.AreEqual(statusCode, status.StatusCode);
65+
Assert.AreEqual("Bad gRPC response. HTTP status code: " + (int)httpStatusCode, status.Detail);
66+
}
67+
68+
[Test]
69+
public async Task AsyncUnaryCall_Non200HttpStatusCodeWithGrpcStatusCode_GrpcStatusCodeTakesPriority()
70+
{
71+
// Arrange
72+
var httpClient = ClientTestHelpers.CreateTestClient(request =>
73+
{
74+
var response = new HttpResponseMessage { StatusCode = HttpStatusCode.Continue };
75+
response.Headers.Add(GrpcProtocolConstants.StatusTrailer, StatusCode.DataLoss.ToString("D"));
76+
return Task.FromResult(response);
77+
});
78+
var invoker = HttpClientCallInvokerFactory.Create(httpClient);
79+
80+
// Act
81+
var call = invoker.AsyncUnaryCall<HelloRequest, HelloReply>(ClientTestHelpers.ServiceMethod, string.Empty, new CallOptions(), new HelloRequest());
82+
83+
// Assert
84+
var ex = await ExceptionAssert.ThrowsAsync<RpcException>(() => call.ResponseAsync).DefaultTimeout();
85+
Assert.AreEqual(StatusCode.DataLoss, ex.StatusCode);
86+
87+
var status = call.GetStatus();
88+
Assert.AreEqual(StatusCode.DataLoss, status.StatusCode);
89+
Assert.AreEqual(null, status.Detail);
90+
}
91+
3492
[Test]
3593
public async Task AsyncUnaryCall_ValidStatusReturned_ReturnsStatus()
3694
{

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ public async Task AsyncClientStreamingCall_NotFoundStatus_ThrowsError()
157157
var ex = await ExceptionAssert.ThrowsAsync<RpcException>(() => call.ResponseAsync).DefaultTimeout();
158158

159159
// Assert
160-
Assert.AreEqual(StatusCode.Cancelled, ex.StatusCode);
161-
Assert.AreEqual("Bad gRPC response. Expected HTTP status code 200. Got status code: 404", ex.Status.Detail);
160+
Assert.AreEqual(StatusCode.Unimplemented, ex.StatusCode);
161+
Assert.AreEqual("Bad gRPC response. HTTP status code: 404", ex.Status.Detail);
162162
}
163163

164164
[Test]

0 commit comments

Comments
 (0)