feat(inbound-api): method-script exception detail on the inbound audit row, never on the HTTP body (D3)
This commit is contained in:
@@ -280,6 +280,16 @@ public static class EndpointExtensions
|
||||
// production call so Replay of a real integration call is complete.
|
||||
trace: method.TraceAllCalls);
|
||||
|
||||
// Flow Debugger step logs (design 2026-09-09 D3): the executor sanitises the
|
||||
// caller-facing message, so this stash is the ONLY place a method script's real
|
||||
// exception survives for the inbound audit row. AuditWriteMiddleware consumes the
|
||||
// slot once when it builds that row; the HTTP body below still carries only the
|
||||
// safe ErrorMessage.
|
||||
if (scriptResult.ErrorDetail is { } scriptErrorDetail)
|
||||
{
|
||||
httpContext.Items[AuditWriteMiddleware.ErrorDetailItemKey] = scriptErrorDetail;
|
||||
}
|
||||
|
||||
if (!scriptResult.Success)
|
||||
{
|
||||
// A client-aborted request is not a script failure.
|
||||
|
||||
@@ -599,8 +599,12 @@ public class InboundScriptExecutor
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Script execution failed for method {Method}", method.Name);
|
||||
// Safe error message, no internal details
|
||||
return new InboundScriptResult(false, null, "Internal script error", "SCRIPT_ERROR");
|
||||
// Safe error message, no internal details — the caller only ever sees this.
|
||||
// Flow Debugger step logs (design 2026-09-09 D3): the exception itself rides the
|
||||
// additive ErrorDetail so the endpoint can put it on the inbound AUDIT row, where
|
||||
// the Flow Debugger's inbound node reads it. It must never reach the HTTP body.
|
||||
return new InboundScriptResult(
|
||||
false, null, "Internal script error", "SCRIPT_ERROR", ErrorDetail: ex.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -743,8 +747,18 @@ public class InboundScriptContext
|
||||
/// <c>SCRIPT_COMPILE_FAILED</c>, <c>SCRIPT_ERROR</c>). Null when the executor does not
|
||||
/// classify the failure; the endpoint then falls back to <c>SCRIPT_ERROR</c>.
|
||||
/// </param>
|
||||
/// <param name="ErrorDetail">
|
||||
/// Flow Debugger step logs (design 2026-09-09 D3): verbose, operator-only failure detail —
|
||||
/// the method script's exception <c>ToString()</c>. <b>Audit-row only.</b> The endpoint stashes
|
||||
/// it on <c>HttpContext.Items[AuditWriteMiddleware.ErrorDetailItemKey]</c> so it lands on the
|
||||
/// inbound audit row (and therefore on the Flow Debugger's inbound node); it is NEVER written
|
||||
/// to the HTTP response, which keeps carrying the sanitised <see cref="ErrorMessage"/>. Null
|
||||
/// for failures whose message already says everything (timeouts, client aborts, site-scope
|
||||
/// denials, compile failures).
|
||||
/// </param>
|
||||
public record InboundScriptResult(
|
||||
bool Success,
|
||||
string? ResultJson,
|
||||
string? ErrorMessage,
|
||||
string? ErrorCode = null);
|
||||
string? ErrorCode = null,
|
||||
string? ErrorDetail = null);
|
||||
|
||||
@@ -79,6 +79,16 @@ public sealed class AuditWriteMiddleware
|
||||
/// </summary>
|
||||
public const string AuditActorItemKey = "ZB.MOM.WW.ScadaBridge.InboundAPI.AuditActor";
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="HttpContext.Items"/> key used by the endpoint handler to publish the
|
||||
/// method script's exception detail when the script threw (Flow Debugger step logs,
|
||||
/// design 2026-09-09 D3). The executor deliberately sanitises the caller-facing
|
||||
/// message, so this slot is the only place the real cause survives; it is consumed
|
||||
/// once here, onto the inbound audit row's <c>ErrorDetail</c>, and never written to
|
||||
/// the HTTP response. An exception that ESCAPED the pipeline takes precedence.
|
||||
/// </summary>
|
||||
public const string ErrorDetailItemKey = "ScadaBridge.Audit.ErrorDetail";
|
||||
|
||||
/// <summary>
|
||||
/// ParentExecutionId: <see cref="HttpContext.Items"/> key under
|
||||
/// which this middleware stashes the inbound request's per-request
|
||||
@@ -318,6 +328,17 @@ public sealed class AuditWriteMiddleware
|
||||
headerDict[header.Key] = value;
|
||||
}
|
||||
|
||||
// Flow Debugger step logs (design 2026-09-09 D3): an exception that ESCAPED the
|
||||
// pipeline is the authoritative failure, so it wins; otherwise the endpoint may
|
||||
// have stashed the method script's exception (which the executor keeps out of the
|
||||
// HTTP body). Absent both, the row carries no detail.
|
||||
var errorDetail = thrown?.ToString();
|
||||
if (errorDetail is null
|
||||
&& ctx.Items.TryGetValue(ErrorDetailItemKey, out var stashedErrorDetail))
|
||||
{
|
||||
errorDetail = stashedErrorDetail as string;
|
||||
}
|
||||
|
||||
var extra = JsonSerializer.Serialize(new
|
||||
{
|
||||
remoteIp = ctx.Connection.RemoteIpAddress?.ToString(),
|
||||
@@ -347,6 +368,7 @@ public sealed class AuditWriteMiddleware
|
||||
httpStatus: statusCode,
|
||||
durationMs: (int)Math.Min(durationMs, int.MaxValue),
|
||||
errorMessage: thrown?.Message,
|
||||
errorDetail: errorDetail,
|
||||
requestSummary: requestBody,
|
||||
responseSummary: responseBody,
|
||||
payloadTruncated: payloadTruncated,
|
||||
|
||||
Reference in New Issue
Block a user