Skip to content

Commit facfde4

Browse files
committed
add support for standardError propagation
1 parent 6a74a11 commit facfde4

File tree

9 files changed

+89
-10
lines changed

9 files changed

+89
-10
lines changed

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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,34 @@ public async Task ServerCallingInexistentCallback_ShouldThrow()
142142
marshalledExceptionType.ShouldBe(typeof(EndpointNotFoundException).FullName);
143143
}
144144

145+
[Theory]
146+
[InlineData("someString")]
147+
[InlineData(2)]
148+
[InlineData(false)]
149+
[InlineData(null)]
150+
public async Task ExceptionDataIsMarshalled(object? value)
151+
{
152+
const string notSerialized = "notSerializedKey";
153+
const string notSerialized2 = "notSerializedKey2";
154+
const string InlineDataKey = "somekey";
155+
const string OnErrorDataKey = "extraData";
156+
Error.SerializableDataKeys.Add(InlineDataKey);
157+
Error.SerializableDataKeys.Add(OnErrorDataKey);
158+
Error.SerializableDataKeys.Remove(notSerialized);
159+
160+
_onError = (callInfo, ex) =>
161+
{
162+
ex.Data.Add(OnErrorDataKey, value);
163+
ex.Data.Add(notSerialized2, value);
164+
return ex;
165+
};
166+
167+
var ex = await Proxy.ThrowWithData(InlineDataKey, value, notSerialized).ShouldThrowAsync<RemoteException>();
168+
ex.Data[InlineDataKey].ShouldBe(value);
169+
ex.Data.Contains(notSerialized).ShouldBeFalse();
170+
ex.Data[OnErrorDataKey].ShouldBe(value);
171+
}
172+
145173
[Fact]
146174
public async Task ServerCallingInexistentCallback_ShouldThrow2()
147175
=> 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)
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 innerEx)
110+
{
111+
ex = new AggregateException(
112+
$"Error while handling error for {request}.",
113+
ex, innerEx);
114+
}
105115
await OnError(request, timeoutHelper.CheckTimeout(ex, request.MethodName));
106116
}
107117
finally

src/UiPath.CoreIpc/Wire/Dtos.cs

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,35 @@ public TResult Deserialize<TResult>()
4848
}
4949
}
5050

51-
public record Error(string Message, string StackTrace, string Type, Error? InnerError)
51+
public record Error(string Message, string StackTrace, string Type, Error? InnerError, IReadOnlyDictionary<string, string>? Data)
5252
{
53+
public static readonly HashSet<string> SerializableDataKeys = ["UiPath.ErrorInfo.Error"];
54+
5355
[return: NotNullIfNotNull("exception")]
5456
public static Error? FromException(Exception? exception)
55-
=> exception is null
56-
? null
57+
=> exception is null
58+
? null
5759
: new(
58-
Message: exception.Message,
59-
StackTrace: exception.StackTrace ?? exception.GetBaseException().StackTrace!,
60-
Type: GetExceptionType(exception),
61-
InnerError: FromException(exception.InnerException));
60+
Message: exception.Message,
61+
StackTrace: exception.StackTrace ?? exception.GetBaseException().StackTrace!,
62+
Type: GetExceptionType(exception),
63+
InnerError: FromException(exception.InnerException),
64+
Data: GetExceptionData(exception));
65+
66+
private static IReadOnlyDictionary<string, string>? GetExceptionData(Exception exception)
67+
{
68+
Dictionary<string, string>? data = null;
69+
foreach (var key in SerializableDataKeys)
70+
{
71+
if (exception.Data.Contains(key))
72+
{
73+
data ??= [];
74+
data[key] = IpcJsonSerializer.Instance.Serialize(exception.Data[key]);
75+
}
76+
}
77+
return data;
78+
}
79+
6280
public override string ToString() => new RemoteException(this).ToString();
6381

6482
private static string GetExceptionType(Exception exception) => (exception as RemoteException)?.Type ?? exception.GetType().FullName!;
@@ -70,6 +88,13 @@ public class RemoteException : Exception
7088
{
7189
Type = error.Type;
7290
StackTrace = error.StackTrace;
91+
if (error.Data != null)
92+
{
93+
foreach (var key in error.Data)
94+
{
95+
Data[key.Key] = IpcJsonSerializer.Instance.Deserialize(key.Value, typeof(object));
96+
}
97+
}
7398
}
7499
public string Type { get; }
75100
public override string StackTrace { get; }

0 commit comments

Comments
 (0)