fix(webhooks): write delivery timestamps as ISO strings
Applies the repo's documented Jest+SQLite guidance (CLAUDE.md) to the webhook delivery path, which was the last one still passing raw Date objects into knex writes. Under jest those store as the literal string "[object Object]", so next_retry_at came back NaN and the retry/backoff test could not assert on it. Production (PG, and SQLite outside jest) was unaffected. Convert the timestamp writes -- and the `next_retry_at <=` due comparison, which has to stay type-consistent with them -- to .toISOString(), matching the existing precedent in downloadJobService.js. Refs testplan REPORT.md #22 (Part 1.2.01).
This commit is contained in:
@@ -146,8 +146,8 @@ describe('webhook delivery worker (#327)', () => {
|
|||||||
payload: JSON.stringify({ id: 'd1', type: 'event.published', data: {} }),
|
payload: JSON.stringify({ id: 'd1', type: 'event.published', data: {} }),
|
||||||
attempt_count: 4,
|
attempt_count: 4,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
next_retry_at: new Date(),
|
next_retry_at: new Date().toISOString(),
|
||||||
created_at: new Date(),
|
created_at: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
await __test.tick();
|
await __test.tick();
|
||||||
|
|
||||||
@@ -190,8 +190,8 @@ describe('webhook delivery worker (#327)', () => {
|
|||||||
payload: JSON.stringify({ id: 'd1', type: 'event.published', data: {} }),
|
payload: JSON.stringify({ id: 'd1', type: 'event.published', data: {} }),
|
||||||
attempt_count: 0,
|
attempt_count: 0,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
next_retry_at: new Date(),
|
next_retry_at: new Date().toISOString(),
|
||||||
created_at: new Date(),
|
created_at: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
await __test.tick();
|
await __test.tick();
|
||||||
|
|
||||||
@@ -214,8 +214,8 @@ describe('webhook delivery worker (#327)', () => {
|
|||||||
payload: JSON.stringify({ id: 'd1', type: 'event.published', data: {} }),
|
payload: JSON.stringify({ id: 'd1', type: 'event.published', data: {} }),
|
||||||
attempt_count: 0,
|
attempt_count: 0,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
next_retry_at: new Date(),
|
next_retry_at: new Date().toISOString(),
|
||||||
created_at: new Date(),
|
created_at: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
await __test.tick();
|
await __test.tick();
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ async function fetchPending(limit) {
|
|||||||
const excludeIds = Array.from(inFlight);
|
const excludeIds = Array.from(inFlight);
|
||||||
let q = db('webhook_deliveries')
|
let q = db('webhook_deliveries')
|
||||||
.where('status', 'pending')
|
.where('status', 'pending')
|
||||||
.where('next_retry_at', '<=', new Date())
|
.where('next_retry_at', '<=', new Date().toISOString())
|
||||||
.orderBy('next_retry_at', 'asc')
|
.orderBy('next_retry_at', 'asc')
|
||||||
.limit(limit);
|
.limit(limit);
|
||||||
if (excludeIds.length > 0) {
|
if (excludeIds.length > 0) {
|
||||||
@@ -71,7 +71,7 @@ async function deliverOne(row) {
|
|||||||
.update({
|
.update({
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
last_error: 'webhook subscription no longer exists',
|
last_error: 'webhook subscription no longer exists',
|
||||||
completed_at: new Date(),
|
completed_at: new Date().toISOString(),
|
||||||
attempt_count: row.attempt_count + 1,
|
attempt_count: row.attempt_count + 1,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -85,7 +85,7 @@ async function deliverOne(row) {
|
|||||||
.update({
|
.update({
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
last_error: 'webhook is disabled',
|
last_error: 'webhook is disabled',
|
||||||
completed_at: new Date(),
|
completed_at: new Date().toISOString(),
|
||||||
attempt_count: row.attempt_count + 1,
|
attempt_count: row.attempt_count + 1,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -172,10 +172,10 @@ async function deliverOne(row) {
|
|||||||
response_body: truncate(stringifyBody(response.data), RESPONSE_TRUNCATE_BYTES),
|
response_body: truncate(stringifyBody(response.data), RESPONSE_TRUNCATE_BYTES),
|
||||||
latency_ms: latency,
|
latency_ms: latency,
|
||||||
attempt_count: newAttempt,
|
attempt_count: newAttempt,
|
||||||
completed_at: new Date(),
|
completed_at: new Date().toISOString(),
|
||||||
next_retry_at: null,
|
next_retry_at: null,
|
||||||
});
|
});
|
||||||
await db('webhooks').where({ id: webhook.id }).update({ last_success_at: new Date() });
|
await db('webhooks').where({ id: webhook.id }).update({ last_success_at: new Date().toISOString() });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,10 +194,10 @@ async function deliverOne(row) {
|
|||||||
last_error: errorMsg,
|
last_error: errorMsg,
|
||||||
latency_ms: latency,
|
latency_ms: latency,
|
||||||
attempt_count: newAttempt,
|
attempt_count: newAttempt,
|
||||||
completed_at: new Date(),
|
completed_at: new Date().toISOString(),
|
||||||
next_retry_at: null,
|
next_retry_at: null,
|
||||||
});
|
});
|
||||||
await db('webhooks').where({ id: webhook.id }).update({ last_failure_at: new Date() });
|
await db('webhooks').where({ id: webhook.id }).update({ last_failure_at: new Date().toISOString() });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,9 +211,9 @@ async function deliverOne(row) {
|
|||||||
last_error: errorMsg,
|
last_error: errorMsg,
|
||||||
latency_ms: latency,
|
latency_ms: latency,
|
||||||
attempt_count: newAttempt,
|
attempt_count: newAttempt,
|
||||||
next_retry_at: new Date(Date.now() + backoff),
|
next_retry_at: new Date(Date.now() + backoff).toISOString(),
|
||||||
});
|
});
|
||||||
await db('webhooks').where({ id: webhook.id }).update({ last_failure_at: new Date() });
|
await db('webhooks').where({ id: webhook.id }).update({ last_failure_at: new Date().toISOString() });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function markFailedFinal(row, reason) {
|
async function markFailedFinal(row, reason) {
|
||||||
@@ -223,10 +223,10 @@ async function markFailedFinal(row, reason) {
|
|||||||
status: 'failed',
|
status: 'failed',
|
||||||
last_error: reason,
|
last_error: reason,
|
||||||
attempt_count: row.attempt_count + 1,
|
attempt_count: row.attempt_count + 1,
|
||||||
completed_at: new Date(),
|
completed_at: new Date().toISOString(),
|
||||||
next_retry_at: null,
|
next_retry_at: null,
|
||||||
});
|
});
|
||||||
await db('webhooks').where({ id: row.webhook_id }).update({ last_failure_at: new Date() });
|
await db('webhooks').where({ id: row.webhook_id }).update({ last_failure_at: new Date().toISOString() });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule the normal retry/backoff for a transient failure that must not
|
// Schedule the normal retry/backoff for a transient failure that must not
|
||||||
@@ -242,7 +242,7 @@ async function scheduleTransientRetry(row, webhook, errorMsg) {
|
|||||||
status: 'failed',
|
status: 'failed',
|
||||||
last_error: errorMsg,
|
last_error: errorMsg,
|
||||||
attempt_count: newAttempt,
|
attempt_count: newAttempt,
|
||||||
completed_at: new Date(),
|
completed_at: new Date().toISOString(),
|
||||||
next_retry_at: null,
|
next_retry_at: null,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -253,10 +253,10 @@ async function scheduleTransientRetry(row, webhook, errorMsg) {
|
|||||||
status: 'pending',
|
status: 'pending',
|
||||||
last_error: errorMsg,
|
last_error: errorMsg,
|
||||||
attempt_count: newAttempt,
|
attempt_count: newAttempt,
|
||||||
next_retry_at: new Date(Date.now() + backoff),
|
next_retry_at: new Date(Date.now() + backoff).toISOString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await db('webhooks').where({ id: webhook.id }).update({ last_failure_at: new Date() });
|
await db('webhooks').where({ id: webhook.id }).update({ last_failure_at: new Date().toISOString() });
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringifyBody(data) {
|
function stringifyBody(data) {
|
||||||
|
|||||||
@@ -190,8 +190,8 @@ async function fire(eventType, data) {
|
|||||||
payload: JSON.stringify(envelope),
|
payload: JSON.stringify(envelope),
|
||||||
attempt_count: 0,
|
attempt_count: 0,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
next_retry_at: now,
|
next_retry_at: now.toISOString(),
|
||||||
created_at: now,
|
created_at: now.toISOString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,8 +232,8 @@ async function enqueueForWebhook(webhookId, eventType, data) {
|
|||||||
payload: JSON.stringify(envelope),
|
payload: JSON.stringify(envelope),
|
||||||
attempt_count: 0,
|
attempt_count: 0,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
next_retry_at: now,
|
next_retry_at: now.toISOString(),
|
||||||
created_at: now,
|
created_at: now.toISOString(),
|
||||||
});
|
});
|
||||||
return { enqueued: true, webhookId: w.id, deliveryId: deliveryUuid };
|
return { enqueued: true, webhookId: w.id, deliveryId: deliveryUuid };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user