Skip to content

Commit 361d233

Browse files
committed
add support for Exception.Data propagation
1 parent 6a74a11 commit 361d233

File tree

11 files changed

+116
-11
lines changed

11 files changed

+116
-11
lines changed

src/Clients/js/src/std/bcl/errors/CoreIpcError.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@ export class CoreIpcError extends Error {
33
super(message);
44
this.name = 'CoreIpcError';
55
}
6+
7+
data?: { [key: string]: any }
68
}

src/UiPath.CoreIpc.Tests/Services/ISystemService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public interface ISystemService
4747
Task<string> DanishNameOfDay(DayOfWeek day, CancellationToken ct);
4848

4949
Task<byte[]> ReverseBytes(byte[] bytes, CancellationToken ct = default);
50+
Task<bool> ThrowWithData(string serializedkey, object? serializedValue, string notSerializedKey);
5051
}
5152

5253
public interface IUnregisteredCallback

src/UiPath.CoreIpc.Tests/Services/SystemService.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,13 @@ public Task<byte[]> ReverseBytes(byte[] bytes, CancellationToken ct = default)
102102
}
103103
return Task.FromResult(bytes);
104104
}
105+
106+
public async Task<bool> ThrowWithData(string serializedkey, object? serializedValue, string notSerializedKey)
107+
{
108+
var ex = new NotImplementedException();
109+
ex.Data[serializedkey] = serializedValue;
110+
ex.Data[notSerializedKey] = serializedValue;
111+
await Task.Yield();
112+
throw ex;
113+
}
105114
}

src/UiPath.CoreIpc.Tests/SystemTests.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,51 @@ public async Task ServerCallingInexistentCallback_ShouldThrow()
142142
marshalledExceptionType.ShouldBe(typeof(EndpointNotFoundException).FullName);
143143
}
144144

145+
#if !NET461 //netframework only works with old style serializable types, so this won't work
146+
147+
[Fact]
148+
public async Task ExceptionDataIsMarshalledForObject()
149+
=> await ExceptionDataIsMarshalled(new ComplexNumber { I = 1, J = 2});
150+
151+
[Fact]
152+
public async Task ExceptionDataIsMarshalledForArray()
153+
=> await ExceptionDataIsMarshalled(new string[] { "bla", "bla" });
154+
155+
#endif
156+
157+
[Theory]
158+
[InlineData("someString")]
159+
[InlineData(2L)]
160+
[InlineData(true)]
161+
[InlineData(null)]
162+
[InlineData(12.34d)]
163+
public async Task ExceptionDataIsMarshalled(object? value)
164+
{
165+
const string notSerialized = "notSerializedKey";
166+
const string notSerialized2 = "notSerializedKey2";
167+
const string InlineDataKey = "somekey";
168+
const string OnErrorDataKey = "extraData";
169+
Error.SerializableDataKeys.Add(InlineDataKey);
170+
Error.SerializableDataKeys.Add(OnErrorDataKey);
171+
Error.SerializableDataKeys.Remove(notSerialized);
172+
173+
_onError = (callInfo, ex) =>
174+
{
175+
ex.Data.Add(OnErrorDataKey, value);
176+
ex.Data.Add(notSerialized2, value);
177+
var readValue = ex.Data[OnErrorDataKey];
178+
readValue.ShouldBe(value);
179+
return ex;
180+
};
181+
182+
var ex = await Proxy.ThrowWithData(InlineDataKey, value, notSerialized).ShouldThrowAsync<RemoteException>();
183+
AsJtokenOrPrimitive(ex.Data[InlineDataKey]).ShouldBeEquivalentTo(AsJtokenOrPrimitive(value));
184+
ex.Data.Contains(notSerialized).ShouldBeFalse();
185+
AsJtokenOrPrimitive(ex.Data[OnErrorDataKey]).ShouldBeEquivalentTo(AsJtokenOrPrimitive(value));
186+
187+
object? AsJtokenOrPrimitive(object? value) => value is null || value.GetType().IsPrimitive ? value : Newtonsoft.Json.Linq.JToken.FromObject(value);
188+
}
189+
145190
[Fact]
146191
public async Task ServerCallingInexistentCallback_ShouldThrow2()
147192
=> await Proxy.AddIncrement(1, 2).ShouldThrowAsync<RemoteException>()

src/UiPath.CoreIpc.Tests/TestBase.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public abstract class TestBase : IAsyncLifetime
2424

2525
protected readonly ConcurrentBag<CallInfo> _serverBeforeCalls = new();
2626
protected BeforeCallHandler? _tailBeforeCall = null;
27+
protected Func<CallInfo?, Exception, Exception>? _onError;
2728

2829
public TestBase(ITestOutputHelper outputHelper)
2930
{
@@ -91,7 +92,8 @@ async Task<IpcServer> Core()
9192
{
9293
_serverBeforeCalls.Add(callInfo);
9394
return _tailBeforeCall?.Invoke(callInfo, ct) ?? Task.CompletedTask;
94-
}
95+
},
96+
OnError = (callInfo, exception) => _onError?.Invoke(callInfo, exception) ?? exception
9597
};
9698

9799
return new()

src/UiPath.CoreIpc/Helpers/Helpers.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ namespace UiPath.Ipc;
1313
internal static class Helpers
1414
{
1515
internal const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly;
16-
internal static Error ToError(this Exception ex) => new(ex.Message, ex.StackTrace ?? ex.GetBaseException().StackTrace!, GetExceptionType(ex), ex.InnerException?.ToError());
17-
private static string GetExceptionType(Exception exception) => (exception as RemoteException)?.Type ?? exception.GetType().FullName!;
16+
internal static Error ToError(this Exception ex) => Error.FromException(ex);
1817
internal static bool Enabled(this ILogger? logger, LogLevel logLevel = LogLevel.Information) => logger is not null && logger.IsEnabled(logLevel);
1918
[Conditional("DEBUG")]
2019
internal static void AssertDisposed(this SemaphoreSlim semaphore) => semaphore.AssertFieldNull("m_waitHandle");

src/UiPath.CoreIpc/Helpers/Router.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,13 @@ public static Route From(IServiceProvider? serviceProvider, ContractSettings end
134134
BeforeCall = endpointSettings.BeforeIncomingCall,
135135
Scheduler = endpointSettings.Scheduler.OrDefault(),
136136
LoggerFactory = serviceProvider.MaybeCreateServiceFactory<ILoggerFactory>(),
137+
OnError = endpointSettings.OnError,
137138
};
138139

139140
public required ServiceFactory Service { get; init; }
140141

141142
public TaskScheduler Scheduler { get; init; }
142143
public BeforeCallHandler? BeforeCall { get; init; }
143144
public Func<ILoggerFactory>? LoggerFactory { get; init; }
145+
public Func<CallInfo?, Exception, Exception>? OnError { get; init; }
144146
}

src/UiPath.CoreIpc/Server/ContractSettings.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ public sealed class ContractSettings
77
public TaskScheduler? Scheduler { get; set; }
88
public BeforeCallHandler? BeforeIncomingCall { get; set; }
99
internal ServiceFactory Service { get; }
10+
public Func<CallInfo?, Exception, Exception>? OnError { get; set; }
1011

1112
internal Type ContractType => Service.Type;
1213
internal object? ServiceInstance => Service.MaybeGetInstance();
1314
internal IServiceProvider? ServiceProvider => Service.MaybeGetServiceProvider();
1415

16+
1517
public ContractSettings(Type contractType, object? serviceInstance = null) : this(
1618
serviceInstance is not null
1719
? new ServiceFactory.Instance()
@@ -40,5 +42,6 @@ internal ContractSettings(ContractSettings other)
4042
Scheduler = other.Scheduler;
4143
BeforeIncomingCall = other.BeforeIncomingCall;
4244
Service = other.Service;
45+
OnError = other.OnError;
4346
}
4447
}

src/UiPath.CoreIpc/Server/Server.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,16 @@ private async ValueTask OnRequestReceived(Request request)
102102
}
103103
catch (Exception ex) when (response is null)
104104
{
105+
try
106+
{
107+
ex = route.OnError?.Invoke(null, ex) ?? ex;
108+
}
109+
catch (Exception handlerEx)
110+
{
111+
ex = new AggregateException(
112+
$"Error while handling error for {request}.",
113+
ex, handlerEx);
114+
}
105115
await OnError(request, timeoutHelper.CheckTimeout(ex, request.MethodName));
106116
}
107117
finally

src/UiPath.CoreIpc/Wire/Dtos.cs

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Diagnostics.CodeAnalysis;
22
using System.Text;
33
using Newtonsoft.Json;
4+
using Newtonsoft.Json.Linq;
45

56
namespace UiPath.Ipc;
67

@@ -48,17 +49,40 @@ public TResult Deserialize<TResult>()
4849
}
4950
}
5051

51-
public record Error(string Message, string StackTrace, string Type, Error? InnerError)
52+
public record Error(string Message, string StackTrace, string Type, Error? InnerError, IReadOnlyDictionary<string, object?>? Data)
5253
{
54+
public static readonly HashSet<string> SerializableDataKeys = ["UiPath.ErrorInfo.Error"];
55+
5356
[return: NotNullIfNotNull("exception")]
5457
public static Error? FromException(Exception? exception)
55-
=> exception is null
56-
? null
58+
=> exception is null
59+
? null
5760
: new(
58-
Message: exception.Message,
59-
StackTrace: exception.StackTrace ?? exception.GetBaseException().StackTrace!,
60-
Type: GetExceptionType(exception),
61-
InnerError: FromException(exception.InnerException));
61+
Message: exception.Message,
62+
StackTrace: exception.StackTrace ?? exception.GetBaseException().StackTrace!,
63+
Type: GetExceptionType(exception),
64+
InnerError: FromException(exception.InnerException),
65+
Data: GetExceptionData(exception));
66+
67+
private static IReadOnlyDictionary<string, object?>? GetExceptionData(Exception exception)
68+
{
69+
Dictionary<string, object?>? data = null;
70+
foreach (var key in SerializableDataKeys)
71+
{
72+
if (exception.Data.Contains(key))
73+
{
74+
data ??= [];
75+
var value = exception.Data[key];
76+
data[key] = value switch
77+
{
78+
null or string or int or bool or Int64 or double or decimal or float => value,
79+
_ => JToken.FromObject(value, IpcJsonSerializer.StringArgsSerializer)
80+
};
81+
}
82+
}
83+
return data;
84+
}
85+
6286
public override string ToString() => new RemoteException(this).ToString();
6387

6488
private static string GetExceptionType(Exception exception) => (exception as RemoteException)?.Type ?? exception.GetType().FullName!;
@@ -70,6 +94,14 @@ public class RemoteException : Exception
7094
{
7195
Type = error.Type;
7296
StackTrace = error.StackTrace;
97+
if (error.Data != null)
98+
{
99+
foreach (var key in error.Data)
100+
{
101+
var value = key.Value;
102+
Data[key.Key] = value;
103+
}
104+
}
73105
}
74106
public string Type { get; }
75107
public override string StackTrace { get; }

0 commit comments

Comments
 (0)