| File: Internal\JsonTranscodingServerCallContext.cs | Web Access |
| Project: src\aspnetcore\src\Grpc\JsonTranscoding\src\Microsoft.AspNetCore.Grpc.JsonTranscoding\Microsoft.AspNetCore.Grpc.JsonTranscoding.csproj (Microsoft.AspNetCore.Grpc.JsonTranscoding) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Sockets; using System.Text; using System.Text.Json; using Grpc.AspNetCore.Server; using Grpc.Core; using Grpc.Shared; using Grpc.Shared.Server; using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.CallHandlers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal; internal sealed class JsonTranscodingServerCallContext : ServerCallContext, IServerCallContextFeature { 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; } public CallHandlerDescriptorInfo DescriptorInfo { get; } public bool IsJsonRequestContent { get; private set; } // Default request encoding to UTF8 so an encoding is available // if the request sends an invalid/unsupported encoding. public Encoding RequestEncoding { get; private set; } = Encoding.UTF8; internal ILogger Logger { get; } private string? _peer; private Metadata? _requestHeaders; private AuthContext? _authContext; public JsonTranscodingServerCallContext(HttpContext httpContext, MethodOptions options, IMethod method, CallHandlerDescriptorInfo descriptorInfo, ILogger logger) { HttpContext = httpContext; Options = options; _method = method; DescriptorInfo = descriptorInfo; Logger = logger; } public void Initialize() { IsJsonRequestContent = JsonRequestHelpers.HasJsonContentType(HttpContext.Request, out var charset); RequestEncoding = JsonRequestHelpers.GetEncodingFromCharset(charset) ?? Encoding.UTF8; // HttpContext.Items is publicly exposed as ServerCallContext.UserState. // Because this is a custom ServerCallContext, HttpContext must be added to UserState so GetHttpContext() continues to work. // https://github.com/grpc/grpc-dotnet/blob/7ef184f3c4cd62fbc3cde55e4bb3e16b58258ca1/src/Grpc.AspNetCore.Server/ServerCallContextExtensions.cs#L53-L61 HttpContext.Items["__HttpContext"] = HttpContext; } public ServerCallContext ServerCallContext => this; protected override string MethodCore => _method.FullName; protected override string HostCore => HttpContext.Request.Host.Value ?? string.Empty; protected override string PeerCore { get { // Follows the standard at https://github.com/grpc/grpc/blob/master/doc/naming.md if (_peer == null) { _peer = BuildPeer(); } return _peer; } } private string BuildPeer() { var connection = HttpContext.Connection; if (connection.RemoteIpAddress != null) { switch (connection.RemoteIpAddress.AddressFamily) { case AddressFamily.InterNetwork: return $"ipv4:{connection.RemoteIpAddress}:{connection.RemotePort}"; case AddressFamily.InterNetworkV6: return $"ipv6:[{connection.RemoteIpAddress}]:{connection.RemotePort}"; default: // TODO(JamesNK) - Test what should be output when used with UDS and named pipes return $"unknown:{connection.RemoteIpAddress}:{connection.RemotePort}"; } } else { return "unknown"; // Match Grpc.Core } } internal async Task ProcessHandlerErrorAsync(Exception ex, string method, bool isStreaming, JsonSerializerOptions options) { Status status; if (ex is RpcException rpcException) { // RpcException is thrown by client code to modify the status returned from the server. // Log the status, detail and debug exception (if present). // Don't log the RpcException itself to reduce log verbosity. All of its information is already captured. GrpcServerLog.RpcConnectionError(Logger, rpcException.StatusCode, rpcException.Status.Detail, rpcException.Status.DebugException); status = rpcException.Status; foreach (var entry in rpcException.Trailers) { ResponseTrailers.Add(entry); } } else { GrpcServerLog.ErrorExecutingServiceMethod(Logger, method, ex); var message = ErrorMessageHelper.BuildErrorMessage("Exception was thrown by handler.", ex, Options.EnableDetailedErrors); // Note that the exception given to status won't be returned to the client. // It is still useful to set in case an interceptor accesses the status on the server. status = new Status(StatusCode.Unknown, message, ex); } await JsonRequestHelpers.SendErrorResponse(HttpContext.Response, RequestEncoding, ResponseTrailers, status, options); if (isStreaming) { await HttpContext.Response.Body.WriteAsync(GrpcProtocolConstants.StreamingDelimiter); } } // Deadline returns max value when there isn't a deadline. protected override DateTime DeadlineCore => DateTime.MaxValue; protected override Metadata RequestHeadersCore { get { if (_requestHeaders == null) { _requestHeaders = new Metadata(); foreach (var header in HttpContext.Request.Headers) { // gRPC metadata contains a subset of the request headers // Filter out pseudo headers (start with :) and other known headers if (header.Key.StartsWith(':') || GrpcProtocolConstants.FilteredHeaders.Contains(header.Key)) { continue; } else if (header.Key.EndsWith(Metadata.BinaryHeaderSuffix, StringComparison.OrdinalIgnoreCase)) { _requestHeaders.Add(header.Key, GrpcProtocolHelpers.ParseBinaryHeader(header.Value!)); } else { _requestHeaders.Add(header.Key, header.Value!); } } } return _requestHeaders; } } protected override CancellationToken CancellationTokenCore => HttpContext.RequestAborted; protected override Metadata ResponseTrailersCore => _responseTrailers ??= new(); protected override Status StatusCore { get; set; } protected override WriteOptions? WriteOptionsCore { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } protected override AuthContext AuthContextCore { get { if (_authContext == null) { var clientCertificate = HttpContext.Connection.ClientCertificate; _authContext = clientCertificate == null ? UnauthenticatedContext : AuthContextHelpers.CreateAuthContext(clientCertificate); } return _authContext; } } protected override IDictionary<object, object> UserStateCore => HttpContext.Items!; protected override ContextPropagationToken CreatePropagationTokenCore(ContextPropagationOptions? options) { throw new NotImplementedException(); } protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders) { // Headers can only be written once. Throw on subsequent call to write response header instead of silent no-op. if (HttpContext.Response.HasStarted) { throw new InvalidOperationException("Response headers can only be sent once per call."); } if (responseHeaders != null) { foreach (var entry in responseHeaders) { if (entry.IsBinary) { HttpContext.Response.Headers[entry.Key] = Convert.ToBase64String(entry.ValueBytes); } else { HttpContext.Response.Headers[entry.Key] = entry.Value; } } } EnsureResponseHeaders(); return HttpContext.Response.BodyWriter.FlushAsync().GetAsTask(); } internal void EnsureResponseHeaders(string? contentType = null) { if (!HttpContext.Response.HasStarted) { HttpContext.Response.StatusCode = StatusCodes.Status200OK; HttpContext.Response.ContentType = contentType ?? MediaType.ReplaceEncoding("application/json", RequestEncoding); } } }