feat(deps): p-retry around Documenso fetch + p-queue installed
p-retry wraps every Documenso API call with 3 attempts (1 + 2 retries), exponential backoff (1s → 4s with jitter). AbortError short-circuits on: - 401/403 — auth failures won't fix themselves on retry - 4xx other than 429 — Documenso rejected the payload; retrying hurts more than it helps 5xx + 429 (rate-limit) go through the retry path with backoff so we politely re-attempt after delay. Recovers the single-connection-blip scenario the audit's services pass flagged. p-queue installed too (audit §36.A.1 companion to p-limit). No concrete land site today — we don't bulk-fan-out to Documenso, and existing pLimit covers our internal mass-op fan-outs. Available for future rate-per-second scenarios. Verified: tsc clean, vitest 1315/1315. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import pRetry, { AbortError } from 'p-retry';
|
||||
|
||||
import { env } from '@/lib/env';
|
||||
import { CodedError } from '@/lib/errors';
|
||||
import { logger } from '@/lib/logger';
|
||||
@@ -22,10 +24,10 @@ async function resolveCreds(portId?: string): Promise<DocumensoCreds> {
|
||||
return { baseUrl: cfg.apiUrl, apiKey: cfg.apiKey, apiVersion: cfg.apiVersion };
|
||||
}
|
||||
|
||||
async function documensoFetch(
|
||||
async function documensoFetchOnce(
|
||||
path: string,
|
||||
options?: RequestInit,
|
||||
portId?: string,
|
||||
options: RequestInit | undefined,
|
||||
portId: string | undefined,
|
||||
): Promise<unknown> {
|
||||
const { baseUrl, apiKey } = await resolveCreds(portId);
|
||||
let res: Response;
|
||||
@@ -40,6 +42,7 @@ async function documensoFetch(
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof FetchTimeoutError) {
|
||||
// Retry timeouts — transient network issue.
|
||||
throw new CodedError('DOCUMENSO_TIMEOUT', {
|
||||
internalMessage: `${path} timed out after ${err.timeoutMs}ms`,
|
||||
});
|
||||
@@ -51,10 +54,24 @@ async function documensoFetch(
|
||||
const err = await res.text();
|
||||
logger.error({ path, status: res.status, err, portId }, 'Documenso API error');
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
throw new CodedError('DOCUMENSO_AUTH_FAILURE', {
|
||||
internalMessage: `${path} → ${res.status}`,
|
||||
});
|
||||
// Auth failures are not retryable — wrong key won't fix itself.
|
||||
throw new AbortError(
|
||||
new CodedError('DOCUMENSO_AUTH_FAILURE', {
|
||||
internalMessage: `${path} → ${res.status}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (res.status >= 400 && res.status < 500 && res.status !== 429) {
|
||||
// 4xx (other than 429) means we sent something Documenso rejected —
|
||||
// retrying won't help. 429 (rate-limit) goes through the retry path
|
||||
// with backoff so we politely re-attempt after delay.
|
||||
throw new AbortError(
|
||||
new CodedError('DOCUMENSO_UPSTREAM_ERROR', {
|
||||
internalMessage: `${path} → ${res.status}: ${err}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
// 5xx + 429 → transient, retry.
|
||||
throw new CodedError('DOCUMENSO_UPSTREAM_ERROR', {
|
||||
internalMessage: `${path} → ${res.status}: ${err}`,
|
||||
});
|
||||
@@ -63,6 +80,39 @@ async function documensoFetch(
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps every Documenso call in p-retry: 3 attempts total (1 + 2 retries)
|
||||
* with exponential backoff (1s, 4s) + jitter. AbortError short-circuits
|
||||
* for auth failures and 4xx-not-429 — those will never succeed on retry.
|
||||
*
|
||||
* This recovers the "single connection blip drops the whole signing flow"
|
||||
* scenario the audit's services pass flagged.
|
||||
*/
|
||||
async function documensoFetch(
|
||||
path: string,
|
||||
options?: RequestInit,
|
||||
portId?: string,
|
||||
): Promise<unknown> {
|
||||
return pRetry(() => documensoFetchOnce(path, options, portId), {
|
||||
retries: 2,
|
||||
factor: 2,
|
||||
minTimeout: 1000,
|
||||
randomize: true,
|
||||
onFailedAttempt: (ctx) => {
|
||||
logger.warn(
|
||||
{
|
||||
path,
|
||||
portId,
|
||||
attempt: ctx.attemptNumber,
|
||||
retriesLeft: ctx.retriesLeft,
|
||||
err: ctx.error.message,
|
||||
},
|
||||
'Documenso fetch retry',
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Documenso 2.x renamed top-level `id` → `documentId` and recipient `id` →
|
||||
// `recipientId`; v1.13 still uses `id`. Normalize both shapes to the legacy
|
||||
// `id` form that this codebase consumes everywhere downstream.
|
||||
|
||||
Reference in New Issue
Block a user