diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamErrors.GeneratedConstructors.cs b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamErrors.GeneratedConstructors.cs
new file mode 100644
index 0000000..b58c881
--- /dev/null
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamErrors.GeneratedConstructors.cs
@@ -0,0 +1,147 @@
+// Copyright 2020-2026 The NATS Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+//
+// Generated constructor surface for JetStream API errors.
+// Source parity: server/jetstream_errors_generated.go
+
+namespace ZB.MOM.NatsNet.Server;
+
+public static partial class JsApiErrors
+{
+ public static JsApiError NewJSAccountResourcesExceededError(params ErrorOption[] opts)
+ {
+ if (ParseOpts(opts) is JsApiError overridden)
+ return Clone(overridden);
+
+ return Clone(AccountResourcesExceeded);
+ }
+
+ public static JsApiError NewJSAtomicPublishContainsDuplicateMessageError(params ErrorOption[] opts)
+ {
+ if (ParseOpts(opts) is JsApiError overridden)
+ return Clone(overridden);
+
+ return Clone(AtomicPublishContainsDuplicateMessage);
+ }
+
+ public static JsApiError NewJSAtomicPublishDisabledError(params ErrorOption[] opts)
+ {
+ if (ParseOpts(opts) is JsApiError overridden)
+ return Clone(overridden);
+
+ return Clone(AtomicPublishDisabled);
+ }
+
+ public static JsApiError NewJSAtomicPublishIncompleteBatchError(params ErrorOption[] opts)
+ {
+ if (ParseOpts(opts) is JsApiError overridden)
+ return Clone(overridden);
+
+ return Clone(AtomicPublishIncompleteBatch);
+ }
+
+ public static JsApiError NewJSAtomicPublishInvalidBatchCommitError(params ErrorOption[] opts)
+ {
+ if (ParseOpts(opts) is JsApiError overridden)
+ return Clone(overridden);
+
+ return Clone(AtomicPublishInvalidBatchCommit);
+ }
+
+ public static JsApiError NewJSAtomicPublishInvalidBatchIDError(params ErrorOption[] opts)
+ {
+ if (ParseOpts(opts) is JsApiError overridden)
+ return Clone(overridden);
+
+ return Clone(AtomicPublishInvalidBatchID);
+ }
+
+ public static JsApiError NewJSAtomicPublishMissingSeqError(params ErrorOption[] opts)
+ {
+ if (ParseOpts(opts) is JsApiError overridden)
+ return Clone(overridden);
+
+ return Clone(AtomicPublishMissingSeq);
+ }
+
+ public static JsApiError NewJSBadRequestError(params ErrorOption[] opts)
+ {
+ if (ParseOpts(opts) is JsApiError overridden)
+ return Clone(overridden);
+
+ return Clone(BadRequest);
+ }
+
+ public static JsApiError NewJSClusterIncompleteError(params ErrorOption[] opts)
+ {
+ if (ParseOpts(opts) is JsApiError overridden)
+ return Clone(overridden);
+
+ return Clone(ClusterIncomplete);
+ }
+
+ public static JsApiError NewJSClusterNotActiveError(params ErrorOption[] opts)
+ {
+ if (ParseOpts(opts) is JsApiError overridden)
+ return Clone(overridden);
+
+ return Clone(ClusterNotActive);
+ }
+
+ public static JsApiError NewJSClusterNotAssignedError(params ErrorOption[] opts)
+ {
+ if (ParseOpts(opts) is JsApiError overridden)
+ return Clone(overridden);
+
+ return Clone(ClusterNotAssigned);
+ }
+
+ public static JsApiError NewJSClusterNotAvailError(params ErrorOption[] opts)
+ {
+ if (ParseOpts(opts) is JsApiError overridden)
+ return Clone(overridden);
+
+ return Clone(ClusterNotAvail);
+ }
+
+ public static JsApiError NewJSClusterNotLeaderError(params ErrorOption[] opts)
+ {
+ if (ParseOpts(opts) is JsApiError overridden)
+ return Clone(overridden);
+
+ return Clone(ClusterNotLeader);
+ }
+
+ public static JsApiError NewJSClusterPeerNotMemberError(params ErrorOption[] opts)
+ {
+ if (ParseOpts(opts) is JsApiError overridden)
+ return Clone(overridden);
+
+ return Clone(ClusterPeerNotMember);
+ }
+
+ public static JsApiError NewJSAtomicPublishTooLargeBatchError(object? size, params ErrorOption[] opts)
+ {
+ if (ParseOpts(opts) is JsApiError overridden)
+ return Clone(overridden);
+
+ return NewWithTags(AtomicPublishTooLargeBatch, "{size}", size);
+ }
+
+ public static JsApiError NewJSAtomicPublishUnsupportedHeaderBatchError(object? header, params ErrorOption[] opts)
+ {
+ if (ParseOpts(opts) is JsApiError overridden)
+ return Clone(overridden);
+
+ return NewWithTags(AtomicPublishUnsupportedHeaderBatch, "{header}", header);
+ }
+
+ public static JsApiError NewJSClusterNoPeersError(Exception err, params ErrorOption[] opts)
+ {
+ if (ParseOpts(opts) is JsApiError overridden)
+ return Clone(overridden);
+
+ return NewWithTags(ClusterNoPeers, "{err}", err);
+ }
+
+}
diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamErrors.cs b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamErrors.cs
index 8182cf6..bb66826 100644
--- a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamErrors.cs
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamErrors.cs
@@ -43,7 +43,7 @@ public sealed class JsApiError
/// Pre-built instances for all JetStream error codes.
/// Mirrors the ApiErrors map in server/jetstream_errors_generated.go.
///
-public static class JsApiErrors
+public static partial class JsApiErrors
{
public delegate object? ErrorOption();
@@ -356,14 +356,10 @@ public static class JsApiErrors
///
public static JsApiError NewJSRestoreSubscribeFailedError(Exception err, string subject, params ErrorOption[] opts)
{
- var overridden = ParseUnless(opts);
- if (overridden != null)
- return overridden;
+ if (ParseOpts(opts) is JsApiError overridden)
+ return Clone(overridden);
- return NewWithTags(
- RestoreSubscribeFailed,
- ("{err}", err.Message),
- ("{subject}", subject));
+ return NewWithTags(RestoreSubscribeFailed, "{subject}", subject, "{err}", err);
}
///
@@ -371,11 +367,10 @@ public static class JsApiErrors
///
public static JsApiError NewJSStreamRestoreError(Exception err, params ErrorOption[] opts)
{
- var overridden = ParseUnless(opts);
- if (overridden != null)
- return overridden;
+ if (ParseOpts(opts) is JsApiError overridden)
+ return Clone(overridden);
- return NewWithTags(StreamRestore, ("{err}", err.Message));
+ return NewWithTags(StreamRestore, "{err}", err);
}
///
@@ -383,20 +378,20 @@ public static class JsApiErrors
///
public static JsApiError NewJSPeerRemapError(params ErrorOption[] opts)
{
- var overridden = ParseUnless(opts);
- return overridden ?? Clone(PeerRemap);
+ if (ParseOpts(opts) is JsApiError overridden)
+ return Clone(overridden);
+
+ return Clone(PeerRemap);
}
- private static JsApiError? ParseUnless(ReadOnlySpan opts)
+ private static object? ParseOpts(params ErrorOption[] opts)
{
- foreach (var opt in opts)
- {
- var value = opt();
- if (value is JsApiError apiErr)
- return Clone(apiErr);
- }
+ object? value = null;
- return null;
+ foreach (var opt in opts)
+ value = opt();
+
+ return value;
}
private static JsApiError Clone(JsApiError source) => new()
@@ -406,13 +401,43 @@ public static class JsApiErrors
Description = source.Description,
};
- private static JsApiError NewWithTags(JsApiError source, params (string key, string value)[] replacements)
+ private static string[] ToReplacerArgs(params object?[] replacements)
+ {
+ var args = new List(replacements.Length);
+ string key = string.Empty;
+
+ for (var i = 0; i < replacements.Length; i++)
+ {
+ if (i % 2 == 0)
+ {
+ key = replacements[i] as string
+ ?? throw new InvalidOperationException("Replacement keys must be strings.");
+ continue;
+ }
+
+ var value = replacements[i] switch
+ {
+ string s => s,
+ Exception ex => ex.Message,
+ null => string.Empty,
+ var other => other.ToString() ?? string.Empty,
+ };
+
+ args.Add(key);
+ args.Add(value);
+ }
+
+ return args.ToArray();
+ }
+
+ private static JsApiError NewWithTags(JsApiError source, params object?[] replacements)
{
var clone = Clone(source);
var description = clone.Description ?? string.Empty;
+ var args = ToReplacerArgs(replacements);
- foreach (var (key, value) in replacements)
- description = description.Replace(key, value, StringComparison.Ordinal);
+ for (var i = 0; i + 1 < args.Length; i += 2)
+ description = description.Replace(args[i], args[i + 1], StringComparison.Ordinal);
clone.Description = description;
return clone;
diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamErrorsGeneratedConstructorsTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamErrorsGeneratedConstructorsTests.cs
new file mode 100644
index 0000000..a0603a0
--- /dev/null
+++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamErrorsGeneratedConstructorsTests.cs
@@ -0,0 +1,39 @@
+// Copyright 2020-2026 The NATS Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+
+using Shouldly;
+
+namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
+
+public sealed class JetStreamErrorsGeneratedConstructorsTests
+{
+ [Fact]
+ public void ConstructorSurface_Group01()
+ {
+ JsApiErrors.NewJSAccountResourcesExceededError().ErrCode.ShouldBe(JsApiErrors.AccountResourcesExceeded.ErrCode);
+ JsApiErrors.NewJSAtomicPublishContainsDuplicateMessageError().ErrCode.ShouldBe(JsApiErrors.AtomicPublishContainsDuplicateMessage.ErrCode);
+ JsApiErrors.NewJSAtomicPublishDisabledError().ErrCode.ShouldBe(JsApiErrors.AtomicPublishDisabled.ErrCode);
+ JsApiErrors.NewJSAtomicPublishIncompleteBatchError().ErrCode.ShouldBe(JsApiErrors.AtomicPublishIncompleteBatch.ErrCode);
+ JsApiErrors.NewJSAtomicPublishInvalidBatchCommitError().ErrCode.ShouldBe(JsApiErrors.AtomicPublishInvalidBatchCommit.ErrCode);
+ JsApiErrors.NewJSAtomicPublishInvalidBatchIDError().ErrCode.ShouldBe(JsApiErrors.AtomicPublishInvalidBatchID.ErrCode);
+ JsApiErrors.NewJSAtomicPublishMissingSeqError().ErrCode.ShouldBe(JsApiErrors.AtomicPublishMissingSeq.ErrCode);
+ JsApiErrors.NewJSBadRequestError().ErrCode.ShouldBe(JsApiErrors.BadRequest.ErrCode);
+ JsApiErrors.NewJSClusterIncompleteError().ErrCode.ShouldBe(JsApiErrors.ClusterIncomplete.ErrCode);
+ JsApiErrors.NewJSClusterNotActiveError().ErrCode.ShouldBe(JsApiErrors.ClusterNotActive.ErrCode);
+ JsApiErrors.NewJSClusterNotAssignedError().ErrCode.ShouldBe(JsApiErrors.ClusterNotAssigned.ErrCode);
+ JsApiErrors.NewJSClusterNotAvailError().ErrCode.ShouldBe(JsApiErrors.ClusterNotAvail.ErrCode);
+ JsApiErrors.NewJSClusterNotLeaderError().ErrCode.ShouldBe(JsApiErrors.ClusterNotLeader.ErrCode);
+ JsApiErrors.NewJSClusterPeerNotMemberError().ErrCode.ShouldBe(JsApiErrors.ClusterPeerNotMember.ErrCode);
+
+ JsApiErrors.NewJSAtomicPublishTooLargeBatchError(512).Description.ShouldBe("atomic publish batch is too large: 512");
+ JsApiErrors.NewJSAtomicPublishUnsupportedHeaderBatchError("Nats-Msg-Id").Description.ShouldBe("atomic publish unsupported header used: Nats-Msg-Id");
+ JsApiErrors.NewJSClusterNoPeersError(new InvalidOperationException("no peers")).Description.ShouldBe("no peers");
+
+ var expected = new JsApiError { Code = 499, ErrCode = 9090, Description = "override" };
+ var fromOverride = JsApiErrors.NewJSAccountResourcesExceededError(JsApiErrors.Unless(expected));
+ fromOverride.Code.ShouldBe(expected.Code);
+ fromOverride.ErrCode.ShouldBe(expected.ErrCode);
+ fromOverride.Description.ShouldBe(expected.Description);
+ ReferenceEquals(fromOverride, expected).ShouldBeFalse();
+ }
+}
diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamErrorsTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamErrorsTests.cs
index 66cc22b..bb153ad 100644
--- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamErrorsTests.cs
+++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamErrorsTests.cs
@@ -1,6 +1,7 @@
// Copyright 2020-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
+using System.Reflection;
using Shouldly;
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
@@ -97,4 +98,39 @@ public sealed class JetStreamErrorsTests
JsApiErrors.NewJSPeerRemapError(JsApiErrors.Unless(new Exception("other error"))),
peerRemap).ShouldBeTrue();
}
+
+ [Fact]
+ public void ParseOpts_WithUnlessApiErrorOption_ReturnsOverride()
+ {
+ var parseOpts = typeof(JsApiErrors).GetMethod(
+ "ParseOpts",
+ BindingFlags.NonPublic | BindingFlags.Static);
+
+ parseOpts.ShouldNotBeNull();
+
+ var expected = new JsApiError { Code = 401, ErrCode = 2048, Description = "override" };
+ var result = parseOpts.Invoke(
+ null,
+ [new JsApiErrors.ErrorOption[] { JsApiErrors.Unless(expected) }]);
+
+ result.ShouldBeAssignableTo()
+ .ErrCode.ShouldBe(expected.ErrCode);
+ }
+
+ [Fact]
+ public void ToReplacerArgs_WithStringErrorAndObject_ConvertsToStringValues()
+ {
+ var toReplacerArgs = typeof(JsApiErrors).GetMethod(
+ "ToReplacerArgs",
+ BindingFlags.NonPublic | BindingFlags.Static);
+
+ toReplacerArgs.ShouldNotBeNull();
+
+ var result = toReplacerArgs.Invoke(
+ null,
+ [new object?[] { "{string}", "value", "{error}", new InvalidOperationException("boom"), "{number}", 42 }]);
+
+ result.ShouldBeAssignableTo()
+ .ShouldBe(["{string}", "value", "{error}", "boom", "{number}", "42"]);
+ }
}
diff --git a/porting.db b/porting.db
index 42fb94b..81e4bc6 100644
Binary files a/porting.db and b/porting.db differ
diff --git a/tools/generate-jetstream-errors.sh b/tools/generate-jetstream-errors.sh
new file mode 100755
index 0000000..9e76d78
--- /dev/null
+++ b/tools/generate-jetstream-errors.sh
@@ -0,0 +1,77 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+output_file="$repo_root/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamErrors.GeneratedConstructors.cs"
+
+simple_methods=(
+ "NewJSAccountResourcesExceededError|AccountResourcesExceeded"
+ "NewJSAtomicPublishContainsDuplicateMessageError|AtomicPublishContainsDuplicateMessage"
+ "NewJSAtomicPublishDisabledError|AtomicPublishDisabled"
+ "NewJSAtomicPublishIncompleteBatchError|AtomicPublishIncompleteBatch"
+ "NewJSAtomicPublishInvalidBatchCommitError|AtomicPublishInvalidBatchCommit"
+ "NewJSAtomicPublishInvalidBatchIDError|AtomicPublishInvalidBatchID"
+ "NewJSAtomicPublishMissingSeqError|AtomicPublishMissingSeq"
+ "NewJSBadRequestError|BadRequest"
+ "NewJSClusterIncompleteError|ClusterIncomplete"
+ "NewJSClusterNotActiveError|ClusterNotActive"
+ "NewJSClusterNotAssignedError|ClusterNotAssigned"
+ "NewJSClusterNotAvailError|ClusterNotAvail"
+ "NewJSClusterNotLeaderError|ClusterNotLeader"
+ "NewJSClusterPeerNotMemberError|ClusterPeerNotMember"
+)
+
+templated_methods=(
+ "NewJSAtomicPublishTooLargeBatchError|object?|size|AtomicPublishTooLargeBatch|{size}"
+ "NewJSAtomicPublishUnsupportedHeaderBatchError|object?|header|AtomicPublishUnsupportedHeaderBatch|{header}"
+ "NewJSClusterNoPeersError|Exception|err|ClusterNoPeers|{err}"
+)
+
+{
+ cat <<'EOF'
+// Copyright 2020-2026 The NATS Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+//
+// Generated constructor surface for JetStream API errors.
+// Source parity: server/jetstream_errors_generated.go
+
+namespace ZB.MOM.NatsNet.Server;
+
+public static partial class JsApiErrors
+{
+EOF
+
+ for entry in "${simple_methods[@]}"; do
+ IFS='|' read -r method field <<<"$entry"
+ cat <"$output_file"
+
+echo "Generated $output_file"