diff --git a/apps/api/src/broker/outboxWorker.ts b/apps/api/src/broker/outboxWorker.ts index 8c661c2..ab9af4f 100644 --- a/apps/api/src/broker/outboxWorker.ts +++ b/apps/api/src/broker/outboxWorker.ts @@ -333,7 +333,23 @@ async function dispatchRow(row: OutboxRow): Promise { return { success: true, conflict: false, hardFail: false, transient: false } } - // Unknown status — treat as transient to avoid silent data loss + // IN-02: an unmapped 4xx (e.g. 405, 409, 422) is a permanent client error — retrying it + // for the full backoff window just delays settling and burns the attempt budget before + // dead-lettering. The transient-eligible 4xx codes (408 request timeout, 429 too many + // requests) are already in TRANSIENT_STATUSES and handled above, so any remaining 4xx + // here is a hard fail. 5xx, network failures, and truly unknown statuses still fall + // through to transient so genuinely recoverable conditions keep their retries. + if (status >= 400 && status < 500) { + return { + success: false, + conflict: false, + hardFail: true, + transient: false, + error: `Hard fail: HTTP ${status} for uid=${row.uid}`, + } + } + + // Unknown / 5xx status — treat as transient to avoid silent data loss return { success: false, conflict: false,