diff --git a/changelog.d/1264-overpass-request-admission.md b/changelog.d/1264-overpass-request-admission.md index 2a4794e1d..e49322df1 100644 --- a/changelog.d/1264-overpass-request-admission.md +++ b/changelog.d/1264-overpass-request-admission.md @@ -1,3 +1,6 @@ - Prevent malformed Overpass filters from blocking the local server, and reject cross-site or excessive query requests before reading their bodies (asturksever, #1264; fixes #1248). +- Release unread responses from the optional Google-token probe so keyless + browser validation can settle; the Overpass gate recognizes only that probe's + expected 404 as an unavailable capability (asturksever, #1264). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 001967ce3..f59f6c5d9 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -4557,6 +4557,12 @@ Retry-After; absent delays back off from 30 seconds to five minutes. Empty remain. Malformed capability-probe responses remain unknown and are retried after the existing 30-second backoff; valid configured decisions remain memoized. Consumer capability misses never schedule automatic retries. +Keyless browser validation also probes the optional Google tiles-token route. +The token client cancels rejected response bodies before returning unavailable, +so they do not remain pending in browser network observation. The Overpass gate +accepts only that same-origin route's expected 404 console diagnostic; other +HTTP and JavaScript errors still fail acceptance. + Operator-configured Overpass area and focus-footprint queries use body-level `out geom` (with `center` for focus), preserving relation member outlines. Member geometry is printed only inside a box around the point (6 km for diff --git a/scripts/qa-overpass-offload.mjs b/scripts/qa-overpass-offload.mjs index e04a05b86..8f6594011 100644 --- a/scripts/qa-overpass-offload.mjs +++ b/scripts/qa-overpass-offload.mjs @@ -306,7 +306,16 @@ try { await observeNetwork(page); page.on('pageerror', (error) => result.errors.push(error.message)); page.on('console', (message) => { - if (message.type() === 'error' && result.consoleErrors.length < 30) + if (message.type() !== 'error') return; + // A keyless server does not offer optional Google tokens. Keep every other + // HTTP or JavaScript error visible, including failures of this route other + // than its expected capability-miss 404. + const optionalTokenMiss = + message.location().url === new URL('/api/google/tiles-token', url).href && + /^Failed to load resource: the server responded with a status of 404\b/.test( + message.text(), + ); + if (!optionalTokenMiss && result.consoleErrors.length < 30) result.consoleErrors.push(message.text()); }); page.on('requestfailed', (request) => { diff --git a/src/maps/googleTokens.js b/src/maps/googleTokens.js index 7896c8f5c..884c5b199 100644 --- a/src/maps/googleTokens.js +++ b/src/maps/googleTokens.js @@ -38,7 +38,10 @@ export function createGoogleTokenSource({ headers: { Accept: 'application/json' }, cache: 'no-store', }); - if (!response.ok) return null; + if (!response.ok) { + await response.body?.cancel(); + return null; + } return parseToken(await response.json(), now()); } catch { return null; diff --git a/src/maps/googleTokens.test.mjs b/src/maps/googleTokens.test.mjs index 87b7cf8dc..d8a1e84ed 100644 --- a/src/maps/googleTokens.test.mjs +++ b/src/maps/googleTokens.test.mjs @@ -50,6 +50,23 @@ test('requests refused for the same token renew it once', async () => { assert.equal(calls.length, 2); }); +test('a refused token response releases its unread body before returning', async () => { + for (const status of [404, 429, 500]) { + let cancelled = false; + const response = new Response( + new ReadableStream({ + cancel() { + cancelled = true; + }, + }), + { status }, + ); + const tokens = createGoogleTokenSource({ fetchImpl: async () => response }); + assert.equal(await tokens.token(), null); + assert.equal(cancelled, true, `HTTP ${status} body released`); + } +}); + test('a server that does not offer tokens yields none', async () => { for (const answer of [ new Response('Not found', { status: 404 }),