feat(inbound-api): method-script exception detail on the inbound audit row, never on the HTTP body (D3)

This commit is contained in:
Joseph Doherty
2026-09-09 12:15:00 -04:00
parent a45857ddd6
commit 98c6809a66
6 changed files with 188 additions and 4 deletions
@@ -265,6 +265,74 @@ public class AuditWriteMiddlewareTests
Assert.Equal("kaboom", evt.ErrorMessage);
}
// ---------------------------------------------------------------------
// 4b. Error detail — the endpoint stashes a method script's exception on
// HttpContext.Items (the HTTP body only ever carries the sanitised
// message), and the middleware lifts it onto the row.
// ---------------------------------------------------------------------
[Fact]
public async Task ScriptErrorDetail_StashedOnItems_LandsOnTheRow()
{
var writer = new RecordingAuditWriter();
var ctx = BuildContext();
const string detail = "System.InvalidOperationException: real reason\n at Script.<Main>";
var mw = CreateMiddleware(hc =>
{
hc.Items[AuditWriteMiddleware.ErrorDetailItemKey] = detail;
hc.Response.StatusCode = 500;
return Task.CompletedTask;
}, writer);
await mw.InvokeAsync(ctx);
var evt = Assert.Single(writer.Events);
Assert.Equal(AuditStatus.Failed, evt.Status);
Assert.Equal(detail, evt.ErrorDetail);
}
[Fact]
public async Task NoErrorDetailStashed_LeavesErrorDetailNull()
{
var writer = new RecordingAuditWriter();
var ctx = BuildContext();
var mw = CreateMiddleware(hc =>
{
hc.Response.StatusCode = 200;
return Task.CompletedTask;
}, writer);
await mw.InvokeAsync(ctx);
var evt = Assert.Single(writer.Events);
Assert.Null(evt.ErrorDetail);
}
[Fact]
public async Task ThrownException_ErrorDetail_WinsOverTheStashedItem()
{
// An exception that ESCAPED the pipeline is the richer, more authoritative
// source: it describes the failure the framework error path is about to report.
var writer = new RecordingAuditWriter();
var ctx = BuildContext();
var boom = new InvalidOperationException("kaboom");
var mw = CreateMiddleware(hc =>
{
hc.Items[AuditWriteMiddleware.ErrorDetailItemKey] = "stashed detail";
throw boom;
}, writer);
await Assert.ThrowsAsync<InvalidOperationException>(() => mw.InvokeAsync(ctx));
var evt = Assert.Single(writer.Events);
// Compared by content, not by an exact ToString(): the re-throw keeps appending
// stack frames after the row was built, so the captured text is a prefix of what
// the exception reads as by the time the assertion runs.
Assert.NotNull(evt.ErrorDetail);
Assert.StartsWith(boom.GetType().FullName + ": kaboom", evt.ErrorDetail);
Assert.DoesNotContain("stashed detail", evt.ErrorDetail);
}
// ---------------------------------------------------------------------
// 5. Actor resolution — the endpoint handler stashes the API key name
// AFTER successful auth so the middleware can pick it up from