in src/StreamJsonRpc/JsonRpc.cs [1969:2151]
private async ValueTask<JsonRpcMessage> DispatchIncomingRequestAsync(JsonRpcRequest request)
{
Requires.NotNull(request, nameof(request));
Requires.Argument(request.Method is not null, nameof(request), "Method must be set.");
CancellationTokenSource? localMethodCancellationSource = null;
CancellationTokenRegistration disconnectedRegistration = default;
try
{
// Add cancelation to inboundCancellationSources before yielding to ensure that
// it cannot be preempted by the cancellation request that would try to set it
// Fix for https://github.com/Microsoft/vs-streamjsonrpc/issues/56
CancellationToken cancellationToken = CancellationToken.None;
if (request.IsResponseExpected)
{
#pragma warning disable CA2000 // Dispose objects before losing scope
localMethodCancellationSource = new CancellationTokenSource();
#pragma warning restore CA2000 // Dispose objects before losing scope
cancellationToken = localMethodCancellationSource.Token;
if (this.CancelLocallyInvokedMethodsWhenConnectionIsClosed)
{
// We do NOT use CancellationTokenSource.CreateLinkedToken because that link is unbreakable,
// and we need to break the link (but without disposing the CTS we created) at the conclusion of this method.
// Disposing the CTS causes .NET Framework (in its default configuration) to throw when CancellationToken.Register is called later,
// which causes problems with some long-lived server methods such as those that return IAsyncEnumerable<T>.
disconnectedRegistration = this.DisconnectedToken.Register(state => ((CancellationTokenSource)state!).Cancel(), localMethodCancellationSource);
}
}
if (this.rpcTargetInfo.TryGetTargetMethod(request, out TargetMethod? targetMethod) && targetMethod.IsFound)
{
// Add cancelation to inboundCancellationSources before yielding to ensure that
// it cannot be preempted by the cancellation request that would try to set it
// Fix for https://github.com/Microsoft/vs-streamjsonrpc/issues/56
if (targetMethod.AcceptsCancellationToken && request.IsResponseExpected)
{
this.CancellationStrategy?.IncomingRequestStarted(request.RequestId, localMethodCancellationSource!);
}
if (this.TraceSource.Switch.ShouldTrace(TraceEventType.Information))
{
this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEvents.LocalInvocation, "Invoking {0}", targetMethod);
}
if (JsonRpcEventSource.Instance.IsEnabled(System.Diagnostics.Tracing.EventLevel.Verbose, System.Diagnostics.Tracing.EventKeywords.None))
{
if (request.IsResponseExpected)
{
JsonRpcEventSource.Instance.ReceivedRequest(request.RequestId.NumberIfPossibleForEvent, request.Method, JsonRpcEventSource.GetArgumentsString(request));
}
else
{
JsonRpcEventSource.Instance.ReceivedNotification(request.Method, JsonRpcEventSource.GetArgumentsString(request));
}
}
return await this.DispatchRequestAsync(request, targetMethod, cancellationToken).ConfigureAwait(false);
}
else
{
ImmutableList<JsonRpc> remoteRpcTargets = this.remoteRpcTargets;
// If we can't find the method or the target object does not exist or does not contain methods, we relay the message to the server.
// Any exceptions from the relay will be returned back to the origin since we catch all exceptions here. The message being relayed to the
// server would share the same id as the message sent from origin. We just take the message objec wholesale and pass it along to the
// other side.
if (!remoteRpcTargets.IsEmpty)
{
if (request.IsResponseExpected)
{
this.CancellationStrategy?.IncomingRequestStarted(request.RequestId, localMethodCancellationSource!);
}
// Yield now so method invocation is async and we can proceed to handle other requests meanwhile.
// IMPORTANT: This should be the first await in this async method,
// and no other await should be between this one and actually invoking the target method.
// This is crucial to the guarantee that method invocation order is preserved from client to server
// when a single-threaded SynchronizationContext is applied.
await this.SynchronizationContextOrDefault;
// Before we forward the request to the remote targets, we need to change the request ID so it gets a new ID in case we run into collisions. For example,
// if origin issues a request destined for the remote target at the same time as a request issued by the relay to the remote target, their IDs could be mixed up.
// See InvokeRemoteTargetWithExistingId unit test for an example.
RequestId previousId = request.RequestId;
JsonRpcMessage? remoteResponse = null;
foreach (JsonRpc remoteTarget in remoteRpcTargets)
{
if (request.IsResponseExpected)
{
request.RequestId = remoteTarget.CreateNewRequestId();
}
CancellationToken token = request.IsResponseExpected ? localMethodCancellationSource!.Token : CancellationToken.None;
remoteResponse = await remoteTarget.InvokeCoreAsync(request, null, token).ConfigureAwait(false);
if (remoteResponse is JsonRpcError error && error.Error is not null)
{
if (error.Error.Code == JsonRpcErrorCode.MethodNotFound || error.Error.Code == JsonRpcErrorCode.InvalidParams)
{
// If the result is an error and that error is method not found or invalid parameters on the remote target, then we continue on to the next target.
continue;
}
}
// Otherwise, we simply return the json response;
break;
}
if (remoteResponse is not null)
{
if (remoteResponse is IJsonRpcMessageWithId messageWithId)
{
messageWithId.RequestId = previousId;
}
return remoteResponse;
}
}
if (targetMethod is null)
{
if (this.TraceSource.Switch.ShouldTrace(TraceEventType.Warning))
{
this.TraceSource.TraceEvent(TraceEventType.Warning, (int)TraceEvents.RequestWithoutMatchingTarget, "No target methods are registered that match \"{0}\".", request.Method);
}
JsonRpcError errorMessage = (this.MessageHandler.Formatter as IJsonRpcMessageFactory)?.CreateErrorMessage() ?? new JsonRpcError();
errorMessage.RequestId = request.RequestId;
errorMessage.Error = new JsonRpcError.ErrorDetail
{
Code = JsonRpcErrorCode.MethodNotFound,
Message = string.Format(CultureInfo.CurrentCulture, Resources.RpcMethodNameNotFound, request.Method),
};
return errorMessage;
}
else
{
if (this.TraceSource.Switch.ShouldTrace(TraceEventType.Warning))
{
this.TraceSource.TraceEvent(TraceEventType.Warning, (int)TraceEvents.RequestWithoutMatchingTarget, "Invocation of \"{0}\" cannot occur because arguments do not match any registered target methods.", request.Method);
}
JsonRpcError errorMessage = (this.MessageHandler.Formatter as IJsonRpcMessageFactory)?.CreateErrorMessage() ?? new JsonRpcError();
errorMessage.RequestId = request.RequestId;
errorMessage.Error = new JsonRpcError.ErrorDetail
{
Code = JsonRpcErrorCode.InvalidParams,
Message = targetMethod.LookupErrorMessage,
Data = targetMethod.ArgumentDeserializationFailures is object ? new CommonErrorData(targetMethod.ArgumentDeserializationFailures) : null,
};
return errorMessage;
}
}
}
catch (Exception ex) when (!this.IsFatalException(StripExceptionToInnerException(ex)))
{
JsonRpcError error = this.CreateError(request, ex);
if (error.Error is not null && JsonRpcEventSource.Instance.IsEnabled(System.Diagnostics.Tracing.EventLevel.Warning, System.Diagnostics.Tracing.EventKeywords.None))
{
JsonRpcEventSource.Instance.SendingError(request.RequestId.NumberIfPossibleForEvent, error.Error.Code);
}
return error;
}
finally
{
if (localMethodCancellationSource is not null)
{
this.CancellationStrategy?.IncomingRequestEnded(request.RequestId);
// Be sure to dispose the link to the local method token we created in case it is linked to our long-lived disposal token
// and otherwise cause a memory leak.
#if NETSTANDARD2_1_OR_GREATER
await disconnectedRegistration.DisposeAsync().ConfigureAwait(false);
#else
disconnectedRegistration.Dispose();
#endif
}
}
}