feat(inbound-api): machine-readable error codes per spec (incl. SITE_UNREACHABLE) + 415 for non-JSON bodies
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -4,6 +4,22 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
|
||||
/// <summary>
|
||||
/// C3: a routed call failed because the target site could not be reached (no contact /
|
||||
/// unreachable). Derives from <see cref="InvalidOperationException"/> so existing
|
||||
/// callers that catch the base type are unaffected, while
|
||||
/// <see cref="InboundScriptExecutor"/> catches this more specific type to surface the
|
||||
/// spec's <c>SITE_UNREACHABLE</c> error code instead of a generic <c>SCRIPT_ERROR</c>.
|
||||
/// A routing failure whose message does <em>not</em> indicate unreachability stays a
|
||||
/// plain <see cref="InvalidOperationException"/>.
|
||||
/// </summary>
|
||||
public sealed class SiteUnreachableException : InvalidOperationException
|
||||
{
|
||||
/// <summary>Initializes a new <see cref="SiteUnreachableException"/> with the given message.</summary>
|
||||
/// <param name="message">The routing-level failure message that indicated the site was unreachable.</param>
|
||||
public SiteUnreachableException(string message) : base(message) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Route.To() helper for cross-site calls from inbound API scripts.
|
||||
/// Resolves instance to site, routes via <see cref="IInstanceRouter"/>, blocks until
|
||||
@@ -170,7 +186,7 @@ public class RouteTarget
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
throw RoutingFailure(
|
||||
response.ErrorMessage ?? "Remote script call failed");
|
||||
}
|
||||
|
||||
@@ -218,7 +234,7 @@ public class RouteTarget
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
throw RoutingFailure(
|
||||
response.ErrorMessage ?? "Remote attribute read failed");
|
||||
}
|
||||
|
||||
@@ -282,7 +298,7 @@ public class RouteTarget
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
throw RoutingFailure(
|
||||
response.ErrorMessage ?? "Remote attribute wait failed");
|
||||
}
|
||||
|
||||
@@ -333,7 +349,7 @@ public class RouteTarget
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
throw RoutingFailure(
|
||||
response.ErrorMessage ?? "Remote attribute write failed");
|
||||
}
|
||||
}
|
||||
@@ -350,10 +366,30 @@ public class RouteTarget
|
||||
var siteId = await _instanceLocator.GetSiteIdForInstanceAsync(_instanceCode, cancellationToken);
|
||||
if (siteId == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
// Route the site-resolution failure through the same classifier so a
|
||||
// message indicating the site is unreachable surfaces as SITE_UNREACHABLE;
|
||||
// a plain not-found / no-assigned-site stays a generic InvalidOperationException.
|
||||
throw RoutingFailure(
|
||||
$"Instance '{_instanceCode}' not found or has no assigned site");
|
||||
}
|
||||
|
||||
return siteId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C3: classifies a routing-level failure message. When the message indicates the
|
||||
/// site could not be reached (unreachable / no contact) the failure is a
|
||||
/// <see cref="SiteUnreachableException"/> — so <see cref="InboundScriptExecutor"/>
|
||||
/// can surface the spec's <c>SITE_UNREACHABLE</c> code — otherwise a plain
|
||||
/// <see cref="InvalidOperationException"/>.
|
||||
/// </summary>
|
||||
private static InvalidOperationException RoutingFailure(string message) =>
|
||||
IsUnreachable(message)
|
||||
? new SiteUnreachableException(message)
|
||||
: new InvalidOperationException(message);
|
||||
|
||||
private static bool IsUnreachable(string message) =>
|
||||
message.Contains("unreachable", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("no contact", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("no-contact", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user