Retry notify webhook POST with backoff (2 s / 10 s / 30 s) before dropping (#27)
CI / build + unit (push) Successful in 1m2s

This commit was merged in pull request #27.
This commit is contained in:
2026-09-25 11:48:39 +02:00
parent dca0dfa894
commit 36021ec2e8
6 changed files with 106 additions and 10 deletions
+33 -7
View File
@@ -130,6 +130,8 @@ function jsonSafe<T>(value: T): T {
// key material must never leave the process — replaced with a marker so callers can see the field exists
const REDACTED = 'REDACTED';
// ponytail: hardcoded notify-webhook backoff [2s, 10s, 30s] (~45 s window) — no env knob; tune here if outages run longer
const NOTIFY_RETRY_DELAYS_MS = [2_000, 10_000, 30_000];
/** Config sections that carry secrets: security (PKI keys) and network (wifi psk). */
function redactSection(
name: string,
@@ -441,16 +443,40 @@ export class MeshService implements OnModuleInit, OnModuleDestroy {
}
const body: Record<string, unknown> = { text };
if (from !== undefined) body.from = from;
void fetch(this.notifyUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
}).catch((err: unknown) =>
this.logger.warn('notify webhook failed: ' + (err as Error).message),
);
void this.postNotify(body);
}
}
/** POST the webhook with retries: up to NOTIFY_RETRY_DELAYS_MS.length + 1 attempts total. */
private async postNotify(body: Record<string, unknown>): Promise<void> {
const maxAttempts = NOTIFY_RETRY_DELAYS_MS.length + 1;
for (let attempt = 1; ; attempt++) {
try {
await fetch(this.notifyUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
});
return; // success on any attempt — same as today's single POST
} catch (err: unknown) {
const msg = (err as Error).message;
this.logger.warn(
`notify webhook failed (attempt ${attempt}/${maxAttempts}): ${msg}`,
);
if (attempt === maxAttempts) {
this.logger.warn(`notify dropped after ${attempt} attempts: ${msg}`);
return;
}
await this.sleep(NOTIFY_RETRY_DELAYS_MS[attempt - 1]);
}
}
}
/** Injectable so specs can skip the backoff delays. */
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Telemetry records, newest first: TELEMETRY_APP packets from any node — device metrics
* (battery/voltage/uptime/channel+air utilization) and environment sensor readings where nodes have them.