Skip to content

CxODEV-1884: carry a diagnostic message on the structured_error contract (3.8 backport) - #219

Merged
ognjenkatic merged 1 commit into
v3from
CxODEV-1884-3.8
Aug 26, 2026
Merged

CxODEV-1884: carry a diagnostic message on the structured_error contract (3.8 backport)#219
ognjenkatic merged 1 commit into
v3from
CxODEV-1884-3.8

Conversation

@ognjenkatic

@ognjenkatic ognjenkatic commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Closes CxODEV-1884 for the 3.8 line. Backport of #218.

Why a separate release

Consuming services pin 3.8.0. StructuredError and StructuredErrorSerializer are present in 3.8.0, absent in 4.0.0/4.0.1, and present again in 4.1.0, so shipping only on master would force those consumers from 3.8.0 to 4.2.0 — a major jump across a line that already dropped and restored this contract once. This lets them pick up the fix in place while master gets the natural 4.2.0 progression.

The patch is identical to #218: all five touched files were byte-identical between v3.8.0 and master (and v3.8.0 is exactly origin/v3's tip), so this is the same change, not a re-implementation.

Version: 3.8.1, not a 3.8.0 suffix

Released as 3.8.1. Under semver a hyphenated suffix is a pre-release3.8.0-fix1884 sorts before 3.8.0, so consumers on 3.8.0 would never be offered it as an upgrade, defeating the point. If a ticket reference in the version is wanted, 3.8.1-fix1884 sorts correctly.

Bumped: Engine, Client, KafkaCancellationNotifier, Patterns. Toolkit stays at 3.0.1-beta3 — it tracks separately on this branch.

Change

StructuredError gains a Message property populated from the exception. No Message property is added to the exception itself — it already has one as an Exception, and the new constructors set it via base(message ?? reason).

message trails referenceError in the new constructors: overload resolution cannot disambiguate (code, reason, referenceError, message) from the existing (code, reason, referenceError, innerException) on an untyped null fourth argument, and putting message third would place that collision on (code, reason, message, null) — the common "message, no drill-down URI" case.

Existing constructors are untouched and new ones added as overloads, so existing three-argument call sites keep binding exactly as before. message is emitted only when it differs from reason, so existing payloads are byte-identical and the shape stays at version 1.

Both execution managers' duplicated mapping is extracted into StructuredError.FromException, which the tests now call instead of mirroring.

Scope note

Unlike master, this branch has no Signal* infrastructure in ConductorSharp.Patterns — that producer path is 4.x-only. This backport is purely the exception path, which is why it stays small.

Verification

62/62 tests pass; the five changed files are csharpier-clean.

Two things worth knowing about this branch: .github/workflows/dotnet.yml only triggers on PRs targeting master, so no CI runs here — the run above was local. And a number of pre-existing files on v3 fail csharpier --check; none are touched by this PR, and no formatting sweep is included.

🤖 Generated with Claude Code

Backport of the 4.2.0 change to the 3.8 line, which consuming services
pin. Identical patch: the five touched files were byte-identical between
v3.8.0 and master.

StructuredErrorException supported only code and reason, so callers that
need both a short, stable reason and a detailed explanation had nowhere to
put the detail and had to fold it into reason. That defeats matching on
reason downstream, and leaves consumers with no separate diagnostic field.

Add StructuredError.Message, populated from the exception. No Message
property is added to the exception itself -- it already has one by virtue
of being an Exception, and the new constructors set it via base(). The
mapping only emits message when it differs from reason, so payloads from
existing call sites are byte-identical and the field is omitted entirely
(NullValueHandling.Ignore), keeping the shape at version 1.

message trails referenceError in the new constructors rather than
following reason. Overload resolution cannot pick between
(code, reason, referenceError, message) and the existing
(code, reason, referenceError, innerException) when the fourth argument is
an untyped null -- and (code, reason, message, null), a message with no
drill-down URI, is the common case. Placing the nullable parameter third
leaves only (code, reason, referenceError, null) ambiguous, which the
three-argument constructor already expresses.

Both execution managers had a hand-copied exception-to-payload mapping and
the test mirrored rather than called it, so a new field could pass tests
while being silently dropped by the type-poll path. Extract the mapping
into StructuredError.FromException and point all three at it.

Released as 3.8.1 rather than a 3.8.0-suffixed build: under semver a
hyphenated suffix is a pre-release and sorts before 3.8.0, so consumers on
3.8.0 would never be offered it as an upgrade.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ognjenkatic
ognjenkatic merged commit 6ce37fd into v3 Aug 26, 2026
@ognjenkatic
ognjenkatic deleted the CxODEV-1884-3.8 branch August 26, 2026 11:23
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.

1 participant