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 @@ -6,8 +6,8 @@
<Product>Json-Rpc.Net ASP.NET Core host</Product>
<Description>ASP.NET Core hosting for AustinHarris.JsonRpc. Adds HTTP endpoints and raw Kestrel connections over TCP, Unix sockets and named pipes, with dependency injection.</Description>
<VersionPrefix>2.0.0</VersionPrefix>
<VersionSuffix>preview.2</VersionSuffix>
<PackageReleaseNotes>2.0.0-preview.2, released with the core package. What is new: https://astn.github.io/JSON-RPC.NET/changelog.html. Upgrading from 1.x: https://astn.github.io/JSON-RPC.NET/upgrading.html</PackageReleaseNotes>
<VersionSuffix>preview.3</VersionSuffix>
<PackageReleaseNotes>2.0.0-preview.3, released with the core package: strong-named assemblies and the error-handler change. What is new: https://astn.github.io/JSON-RPC.NET/changelog.html. Upgrading from 1.x: https://astn.github.io/JSON-RPC.NET/upgrading.html</PackageReleaseNotes>
<Copyright>Austin Harris</Copyright>
<PackageProjectUrl>https://astn.github.io/JSON-RPC.NET/aspnetcore.html</PackageProjectUrl>
<RepositoryUrl>https://github.com/Astn/JSON-RPC.NET</RepositoryUrl>
Expand All @@ -26,6 +26,11 @@
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<PackageIcon>icon.png</PackageIcon>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<!-- Strong-named with the key checked in at the repository root: one key for the four packages and the
friend assemblies, unchanged between releases, so a signed caller can reference the packages and
.NET Framework can load them. -->
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)..\AustinHarris.JsonRpc.snk</AssemblyOriginatorKeyFile>
<RootNamespace>AustinHarris.JsonRpc.AspNetCore</RootNamespace>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
<Product>Json-Rpc.Net Json.NET serializer</Product>
<Description>Json.NET serializer for AustinHarris.JsonRpc. Uses JsonSerializerSettings for converters, contract resolvers and lenient JSON input.</Description>
<VersionPrefix>2.0.0</VersionPrefix>
<VersionSuffix>preview.2</VersionSuffix>
<PackageReleaseNotes>2.0.0-preview.2, released with the core package. What is new: https://astn.github.io/JSON-RPC.NET/changelog.html. Upgrading from 1.x: https://astn.github.io/JSON-RPC.NET/upgrading.html</PackageReleaseNotes>
<VersionSuffix>preview.3</VersionSuffix>
<PackageReleaseNotes>2.0.0-preview.3, released with the core package: strong-named assemblies and the error-handler change. What is new: https://astn.github.io/JSON-RPC.NET/changelog.html. Upgrading from 1.x: https://astn.github.io/JSON-RPC.NET/upgrading.html</PackageReleaseNotes>
<Copyright>Austin Harris</Copyright>
<PackageProjectUrl>https://astn.github.io/JSON-RPC.NET/newtonsoft.html</PackageProjectUrl>
<RepositoryUrl>https://github.com/Astn/JSON-RPC.NET</RepositoryUrl>
Expand All @@ -26,6 +26,11 @@
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<PackageIcon>icon.png</PackageIcon>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<!-- Strong-named with the key checked in at the repository root: one key for the four packages and the
friend assemblies, unchanged between releases, so a signed caller can reference the packages and
.NET Framework can load them. -->
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)..\AustinHarris.JsonRpc.snk</AssemblyOriginatorKeyFile>
<RootNamespace>AustinHarris.JsonRpc.Newtonsoft</RootNamespace>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
<Product>Json-Rpc.Net System.Text.Json serializer</Product>
<Description>System.Text.Json serializer for AustinHarris.JsonRpc. Reads UTF-8 parameters and writes results directly to the response buffer, configured with JsonSerializerOptions.</Description>
<VersionPrefix>2.0.0</VersionPrefix>
<VersionSuffix>preview.2</VersionSuffix>
<PackageReleaseNotes>2.0.0-preview.2, released with the core package. What is new: https://astn.github.io/JSON-RPC.NET/changelog.html. Upgrading from 1.x: https://astn.github.io/JSON-RPC.NET/upgrading.html</PackageReleaseNotes>
<VersionSuffix>preview.3</VersionSuffix>
<PackageReleaseNotes>2.0.0-preview.3, released with the core package: strong-named assemblies and the error-handler change. What is new: https://astn.github.io/JSON-RPC.NET/changelog.html. Upgrading from 1.x: https://astn.github.io/JSON-RPC.NET/upgrading.html</PackageReleaseNotes>
<Copyright>Austin Harris</Copyright>
<PackageProjectUrl>https://astn.github.io/JSON-RPC.NET/systemtextjson.html</PackageProjectUrl>
<RepositoryUrl>https://github.com/Astn/JSON-RPC.NET</RepositoryUrl>
Expand All @@ -25,6 +25,11 @@
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<PackageIcon>icon.png</PackageIcon>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<!-- Strong-named with the key checked in at the repository root: one key for the four packages and the
friend assemblies, unchanged between releases, so a signed caller can reference the packages and
.NET Framework can load them. -->
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)..\AustinHarris.JsonRpc.snk</AssemblyOriginatorKeyFile>
<RootNamespace>AustinHarris.JsonRpc.SystemTextJson</RootNamespace>
</PropertyGroup>

Expand Down
Binary file added AustinHarris.JsonRpc.snk
Binary file not shown.
3 changes: 3 additions & 0 deletions AustinHarris.JsonRpcTestN/AustinHarris.JsonRpcTestN.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<Copyright>Austin Harris</Copyright>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<!-- Friend of the signed core assembly (InternalsVisibleTo), so signed with the same key. -->
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)..\\AustinHarris.JsonRpc.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

<ItemGroup>
Expand Down
13 changes: 8 additions & 5 deletions AustinHarris.JsonRpcTestN/DispatchHardeningTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -385,14 +385,17 @@ public void ExceptionDetails_AreSentWhenEnabled(string name)
{
Config.IncludeExceptionDetails = true;
var response = Parse(Run("{\"method\":\"throwsInner\",\"id\":1}", null, serializer));
// the exception the method threw, with its whole cause chain: the library does not substitute the cause
var data = (JObject)response["error"]["data"];
Assert.AreEqual("System.ArgumentException", (string)data["ClassName"]);
Assert.AreEqual("inner message", (string)data["Message"]);
Assert.AreEqual("System.InvalidOperationException", (string)data["ClassName"]);
Assert.AreEqual("outer message", (string)data["Message"]);
StringAssert.Contains("ThrowsInner", (string)data["StackTraceString"]);
Assert.AreEqual(new ArgumentException().HResult, (int)data["HResult"]);
Assert.AreEqual("innermost message", (string)data["InnerException"]["Message"]);
Assert.AreEqual("System.Exception", (string)data["InnerException"]["ClassName"]);
Assert.AreEqual(new InvalidOperationException().HResult, (int)data["HResult"]);
Assert.AreEqual("System.ArgumentException", (string)data["InnerException"]["ClassName"]);
Assert.AreEqual("inner message", (string)data["InnerException"]["Message"]);
StringAssert.Contains("ThrowsInner", (string)data["InnerException"]["StackTraceString"]);
Assert.AreEqual("System.Exception", (string)data["InnerException"]["InnerException"]["ClassName"]);
Assert.AreEqual("innermost message", (string)data["InnerException"]["InnerException"]["Message"]);

response = Parse(Run("{\"method\":\"throws\",\"id\":1}", null, serializer));
data = (JObject)response["error"]["data"];
Expand Down
166 changes: 166 additions & 0 deletions AustinHarris.JsonRpcTestN/ErrorHandlerExceptionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
using System;
using System.Buffers;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using AustinHarris.JsonRpc;
using Newtonsoft.Json.Linq;
using NUnit.Framework;

namespace AustinHarris.JsonRpcTestN
{
/// <summary>
/// What an error handler receives in <c>error.data</c> when a method throws: the exception the method threw,
/// with its own cause still attached. Only the wrappers the invocation machinery adds are stripped, a
/// <see cref="TargetInvocationException"/> and an <see cref="AggregateException"/> holding one exception; an
/// inner exception is never substituted for the outer one, and an inner <see cref="JsonRpcException"/> is not
/// promoted. The same rule on the synchronous and the asynchronous path.
/// </summary>
[TestFixture]
public sealed class ErrorHandlerExceptionTests
{
private string _session;
private Exception _seen;

[SetUp]
public void SetUp()
{
_session = "error-handler-" + Guid.NewGuid().ToString("N");
_seen = null;
Handler.GetSessionHandler(_session).SetErrorHandler((request, error) => { _seen = error.data as Exception; return error; });
}

[TearDown]
public void TearDown() => Handler.DestroySession(_session);

private static string Request(string method, string id = "1") => "{\"jsonrpc\":\"2.0\",\"method\":\"" + method + "\"" + (id == null ? "" : ",\"id\":" + id) + "}";

private string Sync(string json)
{
var output = new ArrayBufferWriter<byte>();
JsonRpcProcessor.Process(_session, Encoding.UTF8.GetBytes(json).AsSpan(), output);
return Encoding.UTF8.GetString(output.WrittenSpan);
}

private Task<string> Async(string json) => JsonRpcProcessor.ProcessAsync(_session, json);

private static JObject Error(string json, int code)
{
var response = JObject.Parse(json);
Assert.IsNotNull(response["error"], json);
Assert.AreEqual(code, (int)response["error"]["code"], json);
return response;
}

/// <summary>
/// Binds <paramref name="thrower"/> as a synchronous method, a Task method that has already suspended, and a
/// ValueTask method that faults synchronously; "hooked" runs the synchronous method with a pre-process handler
/// installed, which takes the hook path through the request objects.
/// </summary>
private void Bind(Func<Exception> thrower)
{
ServiceBinder.BindMethod(_session, "sync", new Func<int>(() => throw thrower()));
ServiceBinder.BindMethod(_session, "hooked", new Func<int>(() => throw thrower()));
ServiceBinder.BindMethod(_session, "async", new Func<Task<int>>(async () => { await Task.Yield(); throw thrower(); }));
ServiceBinder.BindMethod(_session, "valueTask", new Func<ValueTask<int>>(() => new ValueTask<int>(Task.FromException<int>(thrower()))));
}

private async Task<string> Run(string path, string id = "1")
{
if (path == "hooked")
{
Config.SetPreProcessHandler(_session, (request, context) => null);
return Sync(Request(path, id));
}
return path == "sync" ? Sync(Request(path, id)) : await Async(Request(path, id));
}

[TestCase("sync")] [TestCase("hooked")] [TestCase("async")] [TestCase("valueTask")]
public async Task ThrownException_ReachesTheHandler_WithItsCause(string path)
{
Bind(() => new InvalidOperationException("outer", new IOException("cause")));
string response = await Run(path);
var error = Error(response, -32603);
Assert.AreEqual(JTokenType.Null, error["error"]["data"].Type, "redacted on the wire by default");
Assert.IsInstanceOf<InvalidOperationException>(_seen, "the handler gets the exception that was thrown, not its cause");
Assert.AreEqual("outer", _seen.Message);
Assert.IsInstanceOf<IOException>(_seen.InnerException, "with its cause still attached");
}

[TestCase("sync")] [TestCase("hooked")] [TestCase("async")] [TestCase("valueTask")]
public async Task InnerJsonRpcException_IsNotPromoted(string path)
{
Bind(() => new InvalidOperationException("outer", new JsonRpcException(-32040, "authored inside", "data")));
string response = await Run(path);
Error(response, -32603);
Assert.IsInstanceOf<InvalidOperationException>(_seen);
Assert.AreEqual("outer", _seen.Message);
}

[TestCase("sync")] [TestCase("hooked")] [TestCase("async")] [TestCase("valueTask")]
public async Task AuthoredException_IsPassedAsThrown(string path)
{
Bind(() => new JsonRpcException(-32040, "authored", "ticket 42"));
string response = await Run(path);
var error = Error(response, -32040);
Assert.AreEqual("authored", (string)error["error"]["message"]);
Assert.AreEqual("ticket 42", (string)error["error"]["data"]);
}

[TestCase("sync")] [TestCase("hooked")] [TestCase("async")] [TestCase("valueTask")]
public async Task TargetInvocationWrapper_IsStripped(string path)
{
Bind(() => new TargetInvocationException(new JsonRpcException(-32040, "authored", null)));
Error(await Run(path), -32040);
}

[TestCase("sync", false)] [TestCase("sync", true)] [TestCase("hooked", false)] [TestCase("hooked", true)]
[TestCase("async", false)] [TestCase("async", true)] [TestCase("valueTask", false)] [TestCase("valueTask", true)]
public async Task Aggregate_OnlyASingleInner_IsStripped(string path, bool several)
{
var authored = new JsonRpcException(-32040, "authored", null);
Bind(() => several ? new AggregateException(authored, new Exception("second")) : new AggregateException(authored));
Error(await Run(path), several ? -32603 : -32040);
if (several) Assert.IsInstanceOf<AggregateException>(_seen, "an aggregate of several failures is passed whole");
}

[TestCase("sync")] [TestCase("hooked")] [TestCase("async")] [TestCase("valueTask")]
public async Task Notification_StillReachesTheHandler(string path)
{
Bind(() => new InvalidOperationException("outer", new IOException("cause")));
Assert.AreEqual("", await Run(path, id: null), "a notification produces no response");
Assert.IsInstanceOf<InvalidOperationException>(_seen, "but the handler still sees what was thrown");
Assert.IsInstanceOf<IOException>(_seen.InnerException);
}

[TestCase("sync")] [TestCase("async")]
public async Task ConversionFailure_StaysInvalidParams(string path)
{
ServiceBinder.BindMethod(_session, "sync", new Func<int, int>(n => n));
ServiceBinder.BindMethod(_session, "async", new Func<int, Task<int>>(async n => { await Task.Yield(); return n; }));
string json = "{\"jsonrpc\":\"2.0\",\"method\":\"" + path + "\",\"params\":[\"many\"],\"id\":1}";
string response = path == "sync" ? Sync(json) : await Async(json);
var error = Error(response, -32602);
Assert.AreEqual("conversion", (string)error["error"]["data"]["reason"], response);
}

[TestCase("sync")] [TestCase("hooked")] [TestCase("async")] [TestCase("valueTask")]
public async Task WithDetailsOn_TheWireCarriesTheOuterExceptionAndItsCause(string path)
{
Bind(() => new InvalidOperationException("outer", new IOException("cause")));
Config.IncludeExceptionDetails = true;
try
{
string response = await Run(path);
var data = (JObject)Error(response, -32603)["error"]["data"];
Assert.AreEqual("outer", (string)data["Message"], response);
Assert.AreEqual("cause", (string)data["InnerException"]["Message"], response);
}
finally
{
Config.IncludeExceptionDetails = false;
}
}
}
}
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ the package pages on NuGet link here.
Versions follow [Semantic Versioning](https://semver.org/) for the public API and the documented wire
behaviour: a breaking change to either means a new major version.

## 2.0.0 (in preview: `2.0.0-preview.2`)
## 2.0.0 (in preview: `2.0.0-preview.3`)

### Added

Expand All @@ -33,6 +33,8 @@ behaviour: a breaking change to either means a new major version.

### Changed

- The four assemblies are strong-named (preview.3). One key, `AustinHarris.JsonRpc.snk` at the repository root, signs the packages and the friend assemblies, and stays the same from here on: changing a strong-name key changes the assembly identity. Public key token `e6819c02cf4aec44`. A signed caller can now reference the packages without CS8002, and .NET Framework loads them from a signed assembly.
- An error handler receives the exception the method threw (preview.3). Previously the library replaced an exception that had an `InnerException` with that inner exception, and promoted an inner `JsonRpcException` to the response, so a handler saw the cause instead of the error and an authored outer error lost its message. Now only the wrappers the invocation machinery adds are stripped, a `TargetInvocationException` and a single-inner `AggregateException`, on the synchronous and asynchronous paths alike; everything else, inner exception included, reaches the handler as thrown.
- `RpcMethod.FromMethod` is `RpcMethod.FromMethodInfo`; `RpcInterfaceMethod.Method` is `RpcInterfaceMethod.MethodInfo`. A `MethodInfo` is always spelled out; "method" means the JSON-RPC method.
- The session parameter is spelled `sessionId` on every overload (`BindService`, `Handler.RegisterInstance` and the `JsonRpcService` constructor used `sessionID`).
- `Handler.RegisterFuction` and `UnRegisterFunction` are obsolete; use `ServiceBinder.BindMethod` and `UnbindMethod` (which throw on a duplicate name instead of replacing it).
Expand Down
Loading
Loading