Skip to content

feat: carry selected Exception.Data entries on the Error [ROBO-6164] - #170

Open
eduard-dumitru wants to merge 5 commits into
masterfrom
feature/error-codes
Open

eduard-dumitru wants to merge 5 commits into
masterfrom
feature/error-codes

Conversation

@eduard-dumitru

@eduard-dumitru eduard-dumitru commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Jira: ROBO-6164

What

Lets an endpoint share chosen Exception.Data entries with the caller when a call it honors throws. The values travel on the Error object, not in request headers.

  • Opt-in, per endpoint: IpcBase.PropagatedExceptionDataKeys lists the keys to share.
    • IpcServer applies it to the calls it handles.
    • IpcClient applies it to the callbacks it handles.
    • null or empty (the default) shares nothing.
  • Wire: Error.Data is 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 an Error without it still reads fine.
  • Caller: the entries land in RemoteException.Data, including on inner RemoteExceptions.
  • Call chains (A → B → C): what B received from C travels on to A, whether or not B lists those keys, and whether B rethrows the RemoteException as is or wraps it as an inner exception. A key B lists too takes its current value.
  • API: the only new public member is IpcBase.PropagatedExceptionDataKeys. Error.Data and Error.FromException(exception, dataKeys) are internal.

.NET only for now; the JS and Python clients don't read Error.Data yet.

Testing

  • ExceptionDataTests covers: 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 faulty Exception.Data override.
  • Full suite: 128 passed (net6.0), 126 passed (net461).

Note

This overlaps #169 (trace context) in ServiceClient.cs, Server.cs, ServerConnection.cs, IpcBase.cs, Dtos.cs and Helpers.cs. Server now takes the IpcBase, so #169 only needs to read its setting off it. Whichever merges second needs a small rebase.

🤖 Generated with Claude Code

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 mihainradu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@mihainradu

Copy link
Copy Markdown
Collaborator

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:
Error.FromException(Exception?, IEnumerable?) → internal. Its only caller is Response.Fail, and the tests already see internals. Making it public would lock in this signature while the policy is still likely to evolve (contextual keys, per-type enrichers, pass-through on hops).
Error.Data → internal, keeping [JsonProperty] so the wire stays the same. Newtonsoft handles non-public members that carry the attribute, and the Wire_* tests should confirm it. Consumers read RemoteException.Data, which is plain Exception.Data, so we don't need a new public member for that. It also spares us from documenting the record-equality-by-reference quirk.
PropagatedExceptionDataKeys is the only member that has to be public. If the callback direction isn't needed yet, I'd put it on IpcServer only: moving it up to IpcBase later is binary-compatible, moving it down isn't.
In its XML doc, I'd avoid promising details we may want to change. One is that non-string values are skipped, since we might stringify primitives later. Another is that the list is read when the connection is established, since declaring keys per method would need a per-call read.
The wire shape (Data as a string → string object on each Error level) is the hardest part to take back, since old peers and the JS/Python clients will see it. String-only is the right minimal choice there.

Comment thread src/UiPath.CoreIpc/Server/Server.cs Outdated
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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/UiPath.CoreIpc/Wire/Dtos.cs Outdated
Comment on lines +100 to +111
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.
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/UiPath.CoreIpc/Wire/Dtos.cs Outdated
/// 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@mihainradu

Copy link
Copy Markdown
Collaborator

Scratch the IpcServer-only point: ROBO-6055 needs the callback direction too (executor-facing client), so IpcBase is the right home.

/// 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; }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@mihainradu

Copy link
Copy Markdown
Collaborator

Of my earlier points, two are worth settling here because they're hard to undo after release: keeping Error.FromException(ex, keys) and Error.Data internal (callers read RemoteException.Data), and the doc's "null sends nothing" — if entries on a RemoteException should later pass through hops, that promise breaks, so either pass them through now (with a multi-hop test) or word the doc so it stays open.

eduard-dumitru and others added 4 commits November 2, 2026 15:21
…-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>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants