Repository navigation
feat: carry selected Exception.Data entries on the Error [ROBO-6164] - #170
eduard-dumitru wants to merge 5 commits into
Conversation
An endpoint lists the Exception.Data keys it wants to share in IpcBase.PropagatedExceptionDataKeys (on IpcServer for the calls it handles, on IpcClient for the callbacks it handles). When honoring a call throws, the string values of those keys travel in the new Error.Data, at every level of the inner exception chain, and land in the caller's RemoteException.Data. Nothing is sent by default, and peers that don't know the field ignore it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mihainradu
left a comment
There was a problem hiding this comment.
This matches how I see it: from the IPC's perspective Exception.Data is a black box, string values are good enough, and opt-in via PropagatedExceptionDataKeys, with old peers ignoring the field, is the right default.
Which keys travel is really part of the contract between client and server, so I'd see the endpoint-wide list as the first level. Later, the server could also declare keys contextually while handling a method, or via attributes on the interface/method. Besides the interface, client and server would also share a helper that reads the Exception.Data bag.
Going further, the server side could register, per exception type, funcs/actions that put values from a typed exception into Exception.Data. The client would then read them back with the same shared helper.
On chains of IPC calls (A → B → C): C's entries land in B's RemoteException.Data, but on the way back to A they get filtered again through B's key list. So a hop that doesn't list a key silently drops what was emitted deep down the chain. Entries that arrived on a RemoteException were already shared by their origin, so I'd pass them through as they are. Worth a multi-hop test either way.
None of this needs to be in this PR, but I'd say this is the direction.
|
On the public surface, I'd keep this PR to the minimum we can't avoid, and lean each choice toward what can be loosened later without a breaking change: |
| public string DebugName => _connection.DebugName; | ||
|
|
||
| public Server(Router router, TimeSpan? requestTimeout, Connection connection, IClient? client = null) | ||
| public Server(Router router, TimeSpan? requestTimeout, IReadOnlyCollection<string>? propagatedExceptionDataKeys, Connection connection, IClient? client = null) |
There was a problem hiding this comment.
Server could take the IpcBase and read RequestTimeout / PropagatedExceptionDataKeys itself — both call sites pass them off the same object, and #169 adds a third the same way, which is most of the conflict between the two PRs.
| try | ||
| { | ||
| if (key is not null && exceptionData[key] is string value) | ||
| { | ||
| (data ??= [])[key] = value; | ||
| } | ||
| } | ||
| catch | ||
| { | ||
| // Exception.Data is virtual: one faulty entry must not cost the caller the other entries. | ||
| } | ||
| } |
There was a problem hiding this comment.
One outer try/catch is enough here. The per-key one only covers a custom IDictionary that throws for a single key, which is less likely than a faulty Message override, and that one isn't guarded at all.
|
|
||
| private static Error Deserialize(string json) => (Error)IpcJsonSerializer.Instance.Deserialize(json, typeof(Error))!; | ||
|
|
||
| private sealed class Pair : IAsyncDisposable |
There was a problem hiding this comment.
This Pair is IpcContextTests.Pair plus two properties. Worth one shared helper in Helpers/ that takes the contract, the service and server/client configure callbacks.
| /// The selected <see cref="Exception.Data"/> entries of the exception this error was built from, or <c>null</c> when there are none. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// On the wire this is a JSON object whose values are strings. The record's equality compares this dictionary by reference. |
There was a problem hiding this comment.
Since Data stays string → string, could the remark also say each value is a JSON text, opaque to CoreIpc? Structured payloads then go inside the string, as UiPath.ErrorInfo.Error already does, and the wire never has to widen.
|
Scratch the |
| /// this endpoint handles, at every level of the inner exception chain, landing in the caller's <see cref="RemoteException"/>.<see cref="Exception.Data"/>. | ||
| /// <c>null</c> or empty (the default) sends nothing. It is read when a connection is established. | ||
| /// </summary> | ||
| public IReadOnlyCollection<string>? PropagatedExceptionDataKeys { get; set; } |
There was a problem hiding this comment.
Should UiPath.ErrorInfo.Error be in the list by default? An exception without that key still sends nothing, every UiPath endpoint will want it (otherwise ROBO-6055 sets it on each server and client), and its value is already a JSON text. The only cost I see is CoreIpc naming an ErrorInfo key; an empty list still turns it off.
|
Of my earlier points, two are worth settling here because they're hard to undo after release: keeping |
…-6164] Server takes the IpcBase and reads RequestTimeout and PropagatedExceptionDataKeys itself, once, when it is created. Both call sites passed them off the same object, and #169 adds a third value the same way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ROBO-6164] - Error.Data and Error.FromException(exception, dataKeys) are internal. The wire is unchanged. - One try/catch around gathering the entries: a faulty entry keeps the ones gathered before it. - The docs say each value is an opaque string (structured payloads go inside it, e.g. as JSON) and no longer promise what happens to values that aren't strings or when the key list is read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ExceptionDataTests and IpcContextTests each had their own Pair. Both now use Helpers/IpcPair<TContract>, which takes the service and optional server and client configure callbacks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…O-6164] On A -> B -> C, the RemoteException B gets from C remembers the entries it received, and they travel on to A whether or not B lists their keys, and whether B rethrows it as is or wraps it as an inner exception. A key B lists too takes its current value. Covered by three-endpoint tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jira: ROBO-6164
What
Lets an endpoint share chosen
Exception.Dataentries with the caller when a call it honors throws. The values travel on theErrorobject, not in request headers.IpcBase.PropagatedExceptionDataKeyslists the keys to share.IpcServerapplies it to the calls it handles.IpcClientapplies it to the callbacks it handles.nullor empty (the default) shares nothing.Error.Datais a new optional JSON object of string values, set at every level of the inner exception chain. Each value is opaque to CoreIpc; a structured payload goes inside the string (e.g. as JSON). Older peers ignore the field, and anErrorwithout it still reads fine.RemoteException.Data, including on innerRemoteExceptions.RemoteExceptionas is or wraps it as an inner exception. A key B lists too takes its current value.IpcBase.PropagatedExceptionDataKeys.Error.DataandError.FromException(exception, dataKeys)are internal..NET only for now; the JS and Python clients don't read
Error.Datayet.Testing
ExceptionDataTestscovers: listed and unlisted keys, the default, inner and aggregate exceptions, values that aren't strings, timeouts, callbacks in both directions, three-endpoint chains (rethrown and wrapped), the wire format with old and newer peers, and a faultyException.Dataoverride.Note
This overlaps #169 (trace context) in
ServiceClient.cs,Server.cs,ServerConnection.cs,IpcBase.cs,Dtos.csandHelpers.cs.Servernow takes theIpcBase, so #169 only needs to read its setting off it. Whichever merges second needs a small rebase.🤖 Generated with Claude Code