Skip to content

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:

{ "generation": "0.502.0-alpha.1", "scheduled": 42 }

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.

See also