Files
gods-eye-view/server/providers/overpass.js
T
Said TurkseverandClaude Opus 5.5 0c20ed9975 fix(overpass): retry a joined caller's admission on its own quota
A caller that joins a cold reservation no longer inherits another client's
RATE_LIMITED result. Once the rejected reservation is released it retries:
it either owns the next reservation and is admitted on its own quota, or joins
an admitted fetch. Adds a two-client regression (exhausted owner, available
joiner, exhausted joiner coalescing onto the admitted fetch).

Also drops the duplicate memory publication for a disk hit in the handler;
preflight already sets it, the handler only trims.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 12:33:05 +01:00

244 lines
8.7 KiB
JavaScript

import { makeRateLimiter, clientKey } from './common/rate-limit.js';
import { readRequestBodyCapped } from './common/request.js';
import { admitSameSite } from './common/same-site.js';
import {
OVERPASS_MAX_BODY_BYTES,
OVERPASS_MAX_CONCURRENT,
resolveOverpassUpstreams,
} from './overpass/constants.js';
import { sanitizeOverpassBody } from './overpass/query.js';
import {
resolveOverpassPreflight,
_overpassCache,
readOverpassDisk,
overpassDiskTtlMs,
readStaleOverpass,
trimOverpassCache,
writeOverpassDisk,
} from './overpass/cache.js';
import {
overpassPayloadIsData,
fetchOverpassPayload,
overpassNotConfigured,
} from './overpass/transport.js';
import { installRouteMiddleware } from './places/routes.js';
/** @type {Map<string,Promise>} In-flight Overpass requests keyed by normalized query body. */
const _overpassInFlight = new Map();
let _overpassConcurrent = 0;
// Bound body-reading and validation work, including malformed requests and
// cache hits. Keep the tighter upstream budget below independent of this gate.
const _overpassRequestLimiter = makeRateLimiter({
windowMs: 60_000,
max: 300,
globalMax: 900,
});
const _overpassRateLimiter = makeRateLimiter({
windowMs: 60_000,
max: 90,
globalMax: 300,
});
/**
* Write a completed Overpass payload to the HTTP response.
*
* @param {import('http').ServerResponse} res - Node HTTP response.
* @param {{status:number,body:string,contentType:string,endpoint:string}} payload
* @param {string} [cacheStatus='MISS'] - 'HIT', 'MISS', or 'INFLIGHT'.
*/
function sendOverpassResponse(res, payload, cacheStatus = 'MISS') {
res.writeHead(payload.status, {
'Content-Type': payload.contentType || 'application/json',
'Cache-Control': overpassPayloadIsData(payload)
? 'public, max-age=15'
: 'no-store',
'X-Overpass-Cache': cacheStatus,
...(Number.isFinite(payload.cachedAt)
? { 'X-Overpass-Cached-At': new Date(payload.cachedAt).toISOString() }
: {}),
...(payload.retryAfterMs
? { 'Retry-After': String(Math.ceil(payload.retryAfterMs / 1000)) }
: {}),
});
res.end(payload.body || '');
}
/**
* Vite plugin: Overpass API proxy with response caching and request coalescing.
*
* Accepts POST requests at /api/overpass, normalizes the query body for
* cache keying, and uses only operator-configured endpoints with per-upstream
* timeout and rate-limit detection. Successful responses are cached for
* OVERPASS_CACHE_MS. Concurrent identical queries share a single upstream
* request via the in-flight map.
*
* @returns {import('vite').Plugin}
*/
function overpassProxy({ routing = {} } = {}) {
const installMiddleware = (server) => {
server.middlewares.use('/api/overpass', async (req, res) => {
// Hoisted out of the try so the catch's serve-stale lookup can see it
// (a body-read failure would otherwise hit an out-of-scope reference).
let cacheKey = null;
try {
// Capability probe: lets clients skip queries when nothing is configured.
if (req.method === 'GET' && req.url?.split('?')[0] === '/status') {
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
res.end(
JSON.stringify({
configured: resolveOverpassUpstreams().length > 0,
}),
);
return;
}
if (req.method !== 'POST') {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
return;
}
if (admitSameSite(req, res)) return;
if (!_overpassRequestLimiter(clientKey(req))) {
res.writeHead(429, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
'Retry-After': '60',
});
res.end(JSON.stringify({ error: 'Rate limit exceeded' }));
return;
}
// Collect POST body with a hard byte cap (Overpass QL queries are small)
let body;
try {
body = (
await readRequestBodyCapped(req, OVERPASS_MAX_BODY_BYTES)
).toString();
} catch (err) {
if (err?.code === 'BODY_TOO_LARGE') {
res.writeHead(413, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Overpass query too large' }));
return;
}
throw err;
}
if (!body) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Missing Overpass query body' }));
return;
}
// Validate + clamp the QL: reject unbounded/global queries and cap the
// server-side timeout so a tiny body can't request planet-scale work.
const sanitized = sanitizeOverpassBody(body);
if (!sanitized.ok) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: sanitized.error }));
return;
}
const safeBody = sanitized.body;
// Normalize whitespace so semantically identical Overpass QL queries share cache entries
cacheKey = safeBody.replace(/\s+/g, ' ').trim();
if (!resolveOverpassUpstreams().length) {
const cached = await readStaleOverpass(cacheKey);
sendOverpassResponse(
res,
cached || overpassNotConfigured(),
cached ? 'STALE' : 'DISABLED',
);
return;
}
const preflight = await resolveOverpassPreflight({
cacheKey,
memoryCache: _overpassCache,
inFlight: _overpassInFlight,
// Fresh-enough disk entries survive restarts and skip upstream
// requests; boundary-class queries keep their month-long TTL.
readDisk: () =>
readOverpassDisk(cacheKey, overpassDiskTtlMs(cacheKey)),
allowUpstream: () => _overpassRateLimiter(clientKey(req)),
loadUpstream: async () => {
if (_overpassConcurrent >= OVERPASS_MAX_CONCURRENT)
return {
status: 503,
contentType: 'application/json',
retryAfterMs: 2000,
body: JSON.stringify({
error: 'Overpass proxy busy — try again shortly',
}),
};
_overpassConcurrent++;
try {
const payload = await fetchOverpassPayload(safeBody);
if (overpassPayloadIsData(payload)) {
const entry = { ...payload, cachedAt: Date.now() };
_overpassCache.set(cacheKey, entry);
trimOverpassCache();
writeOverpassDisk(cacheKey, entry);
}
return payload;
} finally {
_overpassConcurrent--;
}
},
});
if (preflight.source === 'RATE_LIMITED') {
res.writeHead(429, {
'Content-Type': 'application/json',
'Retry-After': '5',
});
res.end(JSON.stringify({ error: 'Rate limit exceeded' }));
return;
}
if (!overpassPayloadIsData(preflight.payload)) {
const stale = await readStaleOverpass(cacheKey);
if (stale) {
sendOverpassResponse(res, stale, 'STALE');
return;
}
}
// Preflight already published the disk hit to memory; only trim.
if (preflight.source === 'DISK') trimOverpassCache();
sendOverpassResponse(
res,
preflight.payload,
preflight.source === 'UPSTREAM' ? 'MISS' : preflight.source,
);
} catch (e) {
// Every mirror threw (network-level). Same serve-stale rule.
const stale = cacheKey ? await readStaleOverpass(cacheKey) : null;
if (stale) {
sendOverpassResponse(res, stale, 'STALE');
return;
}
console.error('[Overpass Proxy] request failed');
res.writeHead(502, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Overpass proxy error' }));
}
});
installRouteMiddleware(server.middlewares, routing);
};
return {
name: 'overpass-proxy',
configureServer: installMiddleware,
configurePreviewServer: installMiddleware,
};
}
export { overpassProxy };
export { isOverpassBoundaryQuery } from './overpass/query.js';
export { simplifyOverpassPayloadBody } from './overpass/geometry.js';
export { readOverpassDisk } from './overpass/cache.js';
export { resolveOverpassPreflight } from './overpass/cache.js';
export { overpassPayloadIsData } from './overpass/transport.js';
export { fetchOverpassPayload } from './overpass/transport.js';