- خانه
- خطاها و تلاش مجدد
مفاهیم پایه
خطاها، تلاش مجدد و زمانبندی
پرداخت ردشده استثنا نیست. هر چیزی که استثنا هست از PaymentError سرچشمه میگیرد، و هیچکدام برای شما تلاش مجدد نمیشود.
تمایزی که از همه مهمتر است
پیش از نوشتن بلوک catch، تصمیم بگیرید کدامیک از این دو موقعیت را دارید. به مدیریت کاملاً متضادی نیاز دارند.
| موقعیت | چگونه بروز میکند | معنا |
|---|---|---|
| The payer did not pay |
isSuccessful: false plus errorReason
|
The gateway answered correctly. Your callback ran as designed. Nothing broke. |
| The payer never arrived | No request at all | Abandoned checkout. You find this out from your own reconciliation, not from the gateway. |
| The gateway rejected the request | GatewayProviderError |
Bad merchant id, bad amount, gateway down. Retrying the same request unchanged will fail the same way. |
| The gateway was unreachable | GatewayNetworkError |
This is the only case where a retry has a real chance of working. |
src/http.ts یک AbortController روی مقدار ثابت 10000 میلیثانیه میگذارد و هرگز چیزی را دوباره تلاش نمیکند. گزینه timeoutMs که میتوانست تغییرش دهد روی HttpOptions است، و src/index.ts هرگز آن ماژول را دوباره صادر نمیکند. از کد برنامه امروز، زمانبندی ۱۰ ثانیه است و همینطور هم هست.
تلاش مجدد را خودتان بسازید، روی تنها کلاسی که لازم دارد
GatewayNetworkError را با backoff و jitter تلاش مجدد کنید. GatewayProviderError را هرگز تلاش مجدد نکنید — درگاه به شما حکم داده، نه دچار لغزش — و verifyCallback را هم هرگز خودکار تلاش مجدد نکنید، چون زرینپال فراخوانی دوم را وریفای مجدد میبیند و معنای وریفای تکراری بر عهده شماست.
import {
UnoPay, ZarinpalAdapter, GatewayNetworkError, PaymentError,
type PaymentRequest,
} from 'unopay';
const unopay = new UnoPay({
zarinpal: new ZarinpalAdapter({ merchantId: process.env.ZARINPAL_MERCHANT_ID! }),
});
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
async function createPaymentWithRetry(request: PaymentRequest, attempts = 3) {
for (let i = 1; ; i++) {
try {
return await unopay.createPayment('zarinpal', request);
} catch (err) {
const retryable = err instanceof GatewayNetworkError;
if (!retryable || i >= attempts) throw err;
// exponential backoff with full jitter
await sleep(Math.random() * 2 ** i * 250);
}
}
}
// Never retry a provider verdict:
try {
await createPaymentWithRetry(request);
} catch (err) {
if (err instanceof PaymentError) console.error(err.code, err.message);
throw err;
}
لاگر چه چیزی به شما میگوید
رویدادهای چرخه حیات انواع خرابی را برایتان جدا میکنند: payment_failed و verification_failed برای هر چیز پرتابشده، ولی خود نام رویداد نمیگوید تلاش مجدد ارزش دارد یا نه. کلاس خطا روی cause میگوید.
یک شکاف که ارزش دانستن دارد: بررسی adapter-not-found در createPayment پیش از payment_started اجرا میشود، پس کلید provider اشتباه در سکوت کامل رد میشود. هیچ رویدادی منتشر نمیشود و هیچ خط لاگی ظاهر نمیشود. اگر فراخوانی پرداخت بدون هیچ خروجی لاگ شکست خورد، اول کلید provider را بررسی کنید.