Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ internal static class JsonRequestHelpers
public const string JsonContentType = "application/json";
public const string JsonContentTypeWithCharset = "application/json; charset=utf-8";

public const string StatusDetailsTrailerName = "grpc-status-details-bin";

public static bool HasJsonContentType(HttpRequest request, out StringSegment charset)
{
ArgumentNullException.ThrowIfNull(request);
Expand Down Expand Up @@ -82,21 +84,39 @@ public static (Stream stream, bool usesTranscodingStream) GetStream(Stream inner
}
}

public static async ValueTask SendErrorResponse(HttpResponse response, Encoding encoding, Status status, JsonSerializerOptions options)
public static async ValueTask SendErrorResponse(HttpResponse response, Encoding encoding, Metadata trailers, Status status, JsonSerializerOptions options)
{
if (!response.HasStarted)
{
response.StatusCode = MapStatusCodeToHttpStatus(status.StatusCode);
response.ContentType = MediaType.ReplaceEncoding("application/json", encoding);
}

var e = new Google.Rpc.Status
var e = GetStatusDetails(trailers) ?? new Google.Rpc.Status
{
Message = status.Detail,
Code = (int)status.StatusCode
};

await WriteResponseMessage(response, encoding, e, options, CancellationToken.None);

static Google.Rpc.Status? GetStatusDetails(Metadata trailers)
{
var statusDetails = trailers.Get(StatusDetailsTrailerName);
if (statusDetails?.IsBinary == true)
{
try
{
return Google.Rpc.Status.Parser.ParseFrom(statusDetails.ValueBytes);
}
catch (Exception ex)
{
throw new InvalidOperationException($"Error when parsing the '{StatusDetailsTrailerName}' trailer.", ex);
}
}

return null;
}
}

public static int MapStatusCodeToHttpStatus(StatusCode statusCode)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ internal sealed class JsonTranscodingServerCallContext : ServerCallContext, ISer
private static readonly AuthContext UnauthenticatedContext = new AuthContext(null, new Dictionary<string, List<AuthProperty>>());

private readonly IMethod _method;
private Metadata? _responseTrailers;

public HttpContext HttpContext { get; }
public MethodOptions Options { get; }
Expand Down Expand Up @@ -109,6 +110,10 @@ internal async Task ProcessHandlerErrorAsync(Exception ex, string method, bool i
GrpcServerLog.RpcConnectionError(Logger, rpcException.StatusCode, rpcException.Status.Detail, rpcException.Status.DebugException);

status = rpcException.Status;
foreach (var entry in rpcException.Trailers)
{
ResponseTrailers.Add(entry);
}
}
else
{
Expand All @@ -121,7 +126,7 @@ internal async Task ProcessHandlerErrorAsync(Exception ex, string method, bool i
status = new Status(StatusCode.Unknown, message, ex);
}

await JsonRequestHelpers.SendErrorResponse(HttpContext.Response, RequestEncoding, status, options);
await JsonRequestHelpers.SendErrorResponse(HttpContext.Response, RequestEncoding, ResponseTrailers, status, options);
if (isStreaming)
{
await HttpContext.Response.Body.WriteAsync(GrpcProtocolConstants.StreamingDelimiter);
Expand Down Expand Up @@ -164,7 +169,7 @@ protected override Metadata RequestHeadersCore

protected override CancellationToken CancellationTokenCore => HttpContext.RequestAborted;

protected override Metadata ResponseTrailersCore => throw new NotImplementedException();
protected override Metadata ResponseTrailersCore => _responseTrailers ??= new();

protected override Status StatusCore { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,165 @@ public async Task HandleCallAsync_RpcExceptionThrown_StatusReturned()
Assert.Equal(debugException, exceptionWrite.Exception);
}

[Fact]
public async Task HandleCallAsync_RpcExceptionThrown_StatusDetailsReturned()
{
// Arrange
UnaryServerMethod<JsonTranscodingGreeterService, HelloRequest, HelloReply> invoker = (s, r, c) =>
{
var debugInfo = new Google.Rpc.DebugInfo
{
Detail = "This is some debugging information"
};

var requestInfo = new Google.Rpc.RequestInfo
{
RequestId = "request-id"
};

var badRequest = new Google.Rpc.BadRequest
{
FieldViolations = { new Google.Rpc.BadRequest.Types.FieldViolation { Description = "Negative", Field = "speed" } }
};

var status = new Google.Rpc.Status
{
Code = 123,
Message = "This is a message",
Details =
{
Any.Pack(debugInfo),
Any.Pack(requestInfo),
Any.Pack(badRequest)
}
};

throw new RpcException(new Status(StatusCode.InvalidArgument, "Bad request"),
new Metadata
{
{ JsonRequestHelpers.StatusDetailsTrailerName, status.ToByteArray() }
});
};

var unaryServerCallHandler = CreateCallHandler(invoker,
jsonTranscodingOptions: new GrpcJsonTranscodingOptions()
{
TypeRegistry = TypeRegistry.FromMessages(
Google.Rpc.DebugInfo.Descriptor,
Google.Rpc.RequestInfo.Descriptor,
Google.Rpc.BadRequest.Descriptor)
});

var httpContext = TestHelpers.CreateHttpContext();

// Act
await unaryServerCallHandler.HandleCallAsync(httpContext);

// Assert
Assert.Equal(400, httpContext.Response.StatusCode);

httpContext.Response.Body.Seek(0, SeekOrigin.Begin);
using var responseJson = JsonDocument.Parse(httpContext.Response.Body);
Assert.Equal(123, responseJson.RootElement.GetProperty("code").GetInt32());
Assert.Equal("This is a message", responseJson.RootElement.GetProperty("message").GetString());

var details = responseJson.RootElement.GetProperty("details").EnumerateArray().ToArray();
Assert.Collection(details,
static d =>
{
Assert.Equal("type.googleapis.com/google.rpc.DebugInfo", d.GetProperty("@type").GetString());
Assert.Equal("This is some debugging information", d.GetProperty("detail").GetString());
},
static d =>
{
Assert.Equal("type.googleapis.com/google.rpc.RequestInfo", d.GetProperty("@type").GetString());
Assert.Equal("request-id", d.GetProperty("requestId").GetString());
},
static d =>
{
Assert.Equal("type.googleapis.com/google.rpc.BadRequest", d.GetProperty("@type").GetString());
Assert.Equal(1, d.GetProperty("fieldViolations").GetArrayLength());
});
}

[Fact]
public async Task HandleCallAsync_OtherExceptionThrown_StatusDetailsReturned()
{
// Arrange
UnaryServerMethod<JsonTranscodingGreeterService, HelloRequest, HelloReply> invoker = (s, r, c) =>
{
var debugInfo = new Google.Rpc.DebugInfo
{
Detail = "This is some debugging information"
};

var requestInfo = new Google.Rpc.RequestInfo
{
RequestId = "request-id"
};

var badRequest = new Google.Rpc.BadRequest
{
FieldViolations = { new Google.Rpc.BadRequest.Types.FieldViolation { Description = "Negative", Field = "speed" } }
};

var status = new Google.Rpc.Status
{
Code = 123,
Message = "This is a message",
Details =
{
Any.Pack(debugInfo),
Any.Pack(requestInfo),
Any.Pack(badRequest)
}
};

c.ResponseTrailers.Add(JsonRequestHelpers.StatusDetailsTrailerName, status.ToByteArray());
throw new InvalidOperationException("exception");
};

var unaryServerCallHandler = CreateCallHandler(invoker,
jsonTranscodingOptions: new GrpcJsonTranscodingOptions()
{
TypeRegistry = TypeRegistry.FromMessages(
Google.Rpc.DebugInfo.Descriptor,
Google.Rpc.RequestInfo.Descriptor,
Google.Rpc.BadRequest.Descriptor)
});

var httpContext = TestHelpers.CreateHttpContext();

// Act
await unaryServerCallHandler.HandleCallAsync(httpContext);

// Assert
Assert.Equal(500, httpContext.Response.StatusCode);

httpContext.Response.Body.Seek(0, SeekOrigin.Begin);
using var responseJson = JsonDocument.Parse(httpContext.Response.Body);
Assert.Equal(123, responseJson.RootElement.GetProperty("code").GetInt32());
Assert.Equal("This is a message", responseJson.RootElement.GetProperty("message").GetString());

var details = responseJson.RootElement.GetProperty("details").EnumerateArray().ToArray();
Assert.Collection(details,
static d =>
{
Assert.Equal("type.googleapis.com/google.rpc.DebugInfo", d.GetProperty("@type").GetString());
Assert.Equal("This is some debugging information", d.GetProperty("detail").GetString());
},
static d =>
{
Assert.Equal("type.googleapis.com/google.rpc.RequestInfo", d.GetProperty("@type").GetString());
Assert.Equal("request-id", d.GetProperty("requestId").GetString());
},
static d =>
{
Assert.Equal("type.googleapis.com/google.rpc.BadRequest", d.GetProperty("@type").GetString());
Assert.Equal(1, d.GetProperty("fieldViolations").GetArrayLength());
});
}

[Fact]
public async Task HandleCallAsync_OtherExceptionThrown_StatusReturned()
{
Expand Down