Retry transient foreground requests before surfacing errors

This commit is contained in:
Jonathan Sykes
2026-10-09 21:53:03 +08:00
parent 933e920701
commit d02796b0eb
10 changed files with 282 additions and 26 deletions

View File

@@ -60,6 +60,55 @@ test('frees the key after failure so a retry is possible', async () => {
assert.strictEqual(out, 'ok');
});
test('bounded GET retries recover transient network and server failures', async () => {
const { getJson } = require('./async-guard');
let calls = 0;
const result = await getJson('/api/search', {}, { sleep: async () => {}, fetch: async () => {
calls++;
if (calls === 1) throw new TypeError('connection reset');
if (calls === 2) return new Response('{}', { status: 503 });
return Response.json({ ok: true, results: [] });
} });
assert.equal(result.ok, true);
assert.equal(calls, 3);
});
test('GET retry stops at the bound and does not retry permanent HTTP errors', async () => {
const { getJson } = require('./async-guard');
let calls = 0;
await assert.rejects(getJson('/api/search', {}, { sleep: async () => {}, fetch: async () => {
calls++; return new Response('{}', { status: 404 });
} }), /HTTP 404/);
assert.equal(calls, 1);
await assert.rejects(getJson('/api/search', {}, { attempts: 2, sleep: async () => {}, fetch: async () => {
calls++; throw new TypeError('offline');
} }), /offline/);
assert.equal(calls, 3);
});
test('retryable API error payloads recover but permanent application errors do not retry', async () => {
const { getJson } = require('./async-guard'); let calls = 0;
const retryResult = value => value?.ok === false && /temporarily unavailable/i.test(value.error || '');
const result = await getJson('/api/streams', {}, { retryResult, sleep: async () => {}, fetch: async () => {
calls++;
return Response.json(calls === 1 ? { ok: false, error: 'temporarily unavailable' } : { ok: true, data: [] });
} });
assert.equal(result.ok, true); assert.equal(calls, 2);
calls = 0;
const absent = await getJson('/api/streams', {}, { retryResult, sleep: async () => {}, fetch: async () => { calls++; return Response.json({ ok: false, error: 'video not found' }); } });
assert.equal(absent.ok, false); assert.equal(calls, 1);
});
test('timed out GET bodies retry within the configured bound', async () => {
const { getJson } = require('./async-guard'); let calls = 0;
const result = await getJson('/api/related', {}, { attempts: 2, timeoutMs: 5, sleep: async () => {}, fetch: async (_url, { signal }) => {
calls++;
if (calls === 1) return new Promise((resolve, reject) => signal.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError')), { once: true }));
return Response.json({ ok: true });
} });
assert.equal(result.ok, true); assert.equal(calls, 2);
});
test('supports a synchronous fn', async () => {
const set = new Set();
const out = await runExclusive(set, 's', () => 7);