Operator HTTP API¶
Verified by tests
DeadLetterOperatorEndpointsTests, EFCoreDeadLetterRecoveryServiceTests, DeadLetterRecoverySqlTests — library CI run #31657041675 (2026-08-13)
MapWhizbangDeadLetterEndpoints mounts five routes that wrap the SQL
functions from migrations 050 + 051. Operators get a JSON surface for
inspecting wh_dead_letters and driving recovery — no psql / raw SQL
required.
Mounting¶
using Whizbang.Hosting.AspNet;
var app = builder.Build();
// ...
app.MapWhizbangDeadLetterEndpoints(); // default path "/whizbang/dlq"
app.MapWhizbangDeadLetterEndpoints("/admin/dlq"); // custom prefix
The method returns the RouteGroupBuilder so you can chain authorization,
host filters, and rate limits — recommended before exposing publicly:
app.MapWhizbangDeadLetterEndpoints()
.RequireAuthorization("WhizbangOperator")
.RequireHost("admin.example.com");
Endpoints¶
All responses are JSON; mutation endpoints return 204 No Content on success.
GET /whizbang/dlq/due?max=200¶
Returns up to max rows ready for recovery (skips terminal states
Recovered, PermanentlyFailed, HoldForReview and rows whose
operator_disposition is HoldIndefinitely / MarkPermanentlyFailed).
Default max=200.
Response shape (DeadLetterEntry array):
[
{
"deadLetterId": "019e8b1d-7e90-77cc-a3c7-ff3469de0f33",
"sourceTable": "wh_inbox",
"sourceId": "019e8b1d-7e90-77cc-a3d6-0a826384b4fd",
"streamId": "019e8b1d-7e90-77cc-a3e1-1c5f2a7b9d02",
"messageType": "MyApp.InventoryAdjustCommand",
"failureReason": 5,
"attemptsWhenDlq": 10,
"deadLetteredAt": "2026-06-02T12:34:56Z",
"recoveryStatus": 0,
"recoveryAttempts": 0,
"generation": "0.502.0-alpha.1"
}
]
failureReason is the integer value of MessageFailureReason
(e.g. 5 = MaxAttemptsExceeded, 8 = Throttled). Consumers should
map the enum on the client side. Null-valued fields (e.g. streamId
for singleton-stream messages) are omitted from the JSON — the context
uses JsonIgnoreCondition.WhenWritingNull. Rows come back FIFO-ordered
by dead_lettered_at.
POST /whizbang/dlq/{id}/retry¶
Schedules the row for immediate retry — next_recovery_at = NOW() and
recovery_status back to Pending, so it also resurrects a held row.
The DeadLetterRecoveryWorker picks it up on the next scan (within
milliseconds when the DeadLetterReady NOTIFY signal is wired — migration
056 — otherwise on the next backstop tick). Idempotent — re-issuing the
call just re-sets the timestamp.
POST /whizbang/dlq/{id}/hold¶
Marks the row HoldForReview (terminal). The recovery worker skips it
until an operator explicitly re-issues retry. Use when you've identified
a bug that needs a code fix before the row can succeed.
POST /whizbang/dlq/{id}/give-up¶
Marks the row PermanentlyFailed (terminal). Use when you've decided the
row is truly unrecoverable. Forensic snapshot stays in the table.
POST /whizbang/dlq/scan-now?generation=…¶
Manual generation-replay sweep (reset_dead_letters_for_generation).
Schedules every DLQ row that hasn't yet been retried on the supplied
generation for immediate retry — excluding PermanentlyFailed rows and
rows whose operator disposition is HoldIndefinitely. HoldForReview
rows ARE included: the sweep returns them to Pending, so a new
generation gives held rows one fresh attempt. When the query parameter
is absent, the configured IGenerationProvider.GetGeneration() is used
— typically the running build's identity.
Response:
scheduled is the number of rows whose next_recovery_at got
reset. Idempotent: rows already in retried_on_generations for this
generation are skipped.
AOT compatibility¶
The endpoints use a source-generated JsonSerializerContext
(DeadLetterOperatorJsonContext) — no reflection-based JSON serialization,
so they're safe for Native AOT publication. Custom response types added to
the endpoint group need to be added to the context too.