Skip to content

Commit 05eb543

Browse files
authored
fix(demographics): retry transient WPP page failures (#7580)
* test(demographics): reproduce exhausted WPP retries * fix(demographics): harden WPP page retries
1 parent 3a7b180 commit 05eb543

2 files changed

Lines changed: 67 additions & 3 deletions

File tree

scripts/_demographics-capability-source.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ async function fetchResponse(fetchImpl, url, {
304304
},
305305
signal: requestSignal,
306306
});
307-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
307+
if (!response.ok) throw new Error(`HTTP ${response.status} for ${url}`);
308308
return response;
309309
} catch (error) {
310310
lastError = error;
@@ -326,8 +326,8 @@ export async function fetchWppStage({
326326
for (let offset = 0; offset < locationIds.length; offset += 50) {
327327
const locations = locationIds.slice(offset, offset + 50).join(',');
328328
const [demographics, workingAge] = await Promise.all([
329-
fetchResponse(fetchImpl, `${WPP_BASE}/indicators/67,84,86/locations/${locations}/years/${currentYear}/vars/4/ages/188,1005,1015/sexes/3/cats/0`, { accept: 'application/json', signal }).then((response) => response.json()),
330-
fetchResponse(fetchImpl, `${WPP_BASE}/indicators/70/locations/${locations}/years/${currentYear},${currentYear + 10}/vars/4/ages/40/sexes/3/cats/0`, { accept: 'application/json', signal }).then((response) => response.json()),
329+
fetchResponse(fetchImpl, `${WPP_BASE}/indicators/67,84,86/locations/${locations}/years/${currentYear}/vars/4/ages/188,1005,1015/sexes/3/cats/0`, { accept: 'application/json', signal, attempts: 4 }).then((response) => response.json()),
330+
fetchResponse(fetchImpl, `${WPP_BASE}/indicators/70/locations/${locations}/years/${currentYear},${currentYear + 10}/vars/4/ages/40/sexes/3/cats/0`, { accept: 'application/json', signal, attempts: 4 }).then((response) => response.json()),
331331
]);
332332
demographicsRows.push(...(Array.isArray(demographics) ? demographics : []));
333333
workingAgeRows.push(...(Array.isArray(workingAge) ? workingAge : []));

tests/demographics-capability-seed.test.mjs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
buildDemographicsPayload,
1010
demographicsContentMeta,
1111
demographicsStageCoverageMeta,
12+
fetchWppStage,
1213
parseIlostatWorkforceCsv,
1314
parseWorldBankEducation,
1415
parseWppCapability,
@@ -24,6 +25,26 @@ import { resolveSourceOrigin } from '../scripts/source-origin.mjs';
2425
const fixture = (name) => readFileSync(new URL(`./fixtures/demographics-capability/${name}`, import.meta.url), 'utf8');
2526
const repoFile = (name) => readFileSync(new URL(`../${name}`, import.meta.url), 'utf8');
2627

28+
function wppResponse(url, currentYear = 2026) {
29+
const requestUrl = String(url);
30+
const locations = requestUrl.match(/\/locations\/([^/]+)\//)?.[1].split(',').map(Number) || [];
31+
const shared = { variantId: 4, sexId: 3 };
32+
const rows = requestUrl.includes('/indicators/70/')
33+
? locations.flatMap((locationId) => [
34+
{ ...shared, locationId, indicatorId: 70, ageId: 40, timeLabel: currentYear, value: 1_000_000 },
35+
{ ...shared, locationId, indicatorId: 70, ageId: 40, timeLabel: currentYear + 10, value: 900_000 },
36+
])
37+
: locations.flatMap((locationId) => [
38+
{ ...shared, locationId, indicatorId: 67, ageId: 188, timeLabel: currentYear, value: 40 },
39+
{ ...shared, locationId, indicatorId: 84, ageId: 1005, timeLabel: currentYear, value: 20 },
40+
{ ...shared, locationId, indicatorId: 86, ageId: 1015, timeLabel: currentYear, value: 50 },
41+
]);
42+
return new Response(JSON.stringify(rows), {
43+
status: 200,
44+
headers: { 'Content-Type': 'application/json' },
45+
});
46+
}
47+
2748
describe('demographics capability source parsers (#6437)', () => {
2849
it('parses real WPP JSON without turning null into zero', () => {
2950
const raw = JSON.parse(fixture('wpp.json'));
@@ -76,6 +97,49 @@ describe('demographics capability source parsers (#6437)', () => {
7697
}
7798
assert.equal(validateDemographicsStageCoverage(countries, 'ilostat').trainedIndustrialWorkforcePeople, 150);
7899
});
100+
101+
it('recovers after two consecutive HTTP 502 responses from one WPP page', async () => {
102+
const targetPage = '/indicators/67,84,86/locations/100,104,';
103+
let targetPageRequests = 0;
104+
const result = await fetchWppStage({
105+
currentYear: 2026,
106+
fetchImpl: async (url) => {
107+
if (String(url).includes(targetPage)) {
108+
targetPageRequests += 1;
109+
if (targetPageRequests <= 2) return new Response('Bad Gateway', { status: 502 });
110+
}
111+
return wppResponse(url);
112+
},
113+
});
114+
115+
assert.equal(targetPageRequests, 3);
116+
const recordCount = Object.keys(result.countries).length;
117+
assert.ok(recordCount >= 229);
118+
assert.equal(validateDemographicsStageCoverage(result.countries, 'wpp').medianAgeYears, recordCount);
119+
});
120+
121+
it('identifies the exact WPP page when its HTTP failure is exhausted', async () => {
122+
const failedPage = '/indicators/70/locations/288,292,';
123+
let failedPageRequests = 0;
124+
await assert.rejects(
125+
fetchWppStage({
126+
currentYear: 2026,
127+
fetchImpl: async (url) => {
128+
if (String(url).includes(failedPage)) {
129+
failedPageRequests += 1;
130+
return new Response('Bad Gateway', { status: 502 });
131+
}
132+
return wppResponse(url);
133+
},
134+
}),
135+
(error) => {
136+
assert.match(error.message, /HTTP 502/);
137+
assert.match(error.message, new RegExp(failedPage));
138+
return true;
139+
},
140+
);
141+
assert.equal(failedPageRequests, 4);
142+
});
79143
});
80144

81145
describe('demographics partial-stage publication', () => {

0 commit comments

Comments
 (0)