fix(server): declare the method contract on the three read-only query proxies

/api/opensky, /api/adsblol/mil and /api/route answered any verb with the same
body while ignoring the request entirely. Measured against a running server
before the change: POST /api/opensky returned 200 with 833,759 bytes of live
state vectors, POST /api/adsblol/mil returned 47,372 bytes. Their siblings do
not: /api/geocode, /api/weather and /api/transit answer 405, and /api/overpass
is deliberately POST-only.

These three are read-only query proxies, so they now answer 405 to anything but
GET, using the wording and shape /api/geocode already uses. The refusal comes
first, before rate limiting and before any upstream call, which the tests
assert by stubbing fetch to throw.

Scope, stated plainly: this is a consistency and intent fix. No rate limiter
was being bypassed — /api/route already limits every request regardless of
verb, and the two aircraft routes have no limiter at all, only 9s and 12s
server-side caches that bound upstream cost for any method.

One existing fixture in src/search/geospatial.test.mjs built a request without
a method, which a real Node request always has; it now sets GET.

Verified on a running server after the change: GET still 200 on all three,
POST 405 on all three, /api/overpass and the other proxies unchanged.
Suite goes from 5090 to 5092 tests, 0 failures.
This commit is contained in:
daikaginza committed 2026-09-29 10:52:26 +07:00
1 parent e7707d9a0f
commit b09db55149
7 files changed
+70 -1

No files matched your search

+5 -1
View File
@@ -209,7 +209,11 @@ test('routing middleware uses configured OSRM servers while retaining request bo
const request = async (query) => {
let payload;
await handler(
{ url: `/?${query}`, socket: { remoteAddress: '127.0.0.1' } },
{
url: `/?${query}`,
method: 'GET',
socket: { remoteAddress: '127.0.0.1' },
},
{
writeHead() {},
end(body) {
+23
View File
@@ -381,3 +381,26 @@ test('track backfill proxy returns 502 on an oversized upstream body and caches
assert.equal(res2.statusCode, 502);
assert.equal(callCount, 1);
});
test('the aircraft snapshot routes decline a non-GET before any upstream work', async (t) => {
// Both routes answered every verb with the full snapshot, unlike the query
// proxies beside them, so nothing said whether a body was read or ignored.
// A stubbed fetch that throws proves the refusal happens before the handler
// reaches upstream, rather than after paying for the work and discarding it.
t.mock.method(globalThis, 'fetch', async (url) => {
throw new Error(`upstream must not be reached: ${url}`);
});
const cases = [
[install(providers.openSkyProxy()), '/api/opensky'],
[install(providers.adsbLolProxy()), '/api/adsblol/mil'],
];
for (const [request, route] of cases) {
for (const method of ['POST', 'PUT', 'DELETE', 'PATCH']) {
const response = await request(route, '/', method);
assert.equal(response.statusCode, 405, `${method} ${route}`);
assert.deepEqual(JSON.parse(response.body), {
error: 'Method Not Allowed',
});
}
}
});
+19
View File
@@ -482,3 +482,22 @@ test('the upstream host is pinned: a redirect is refused, not followed', async (
await request('/api/route', '?profile=car&coords=-97,30;-97.01,30.01');
assert.deepEqual(seen, ['error'], 'a redirect would escape the pinned host');
});
test('the routing proxy declines a non-GET before rate limiting or upstream', async (t) => {
// The documented shape is a GET with query parameters, but every verb was
// answered alike. Refuse first: a stubbed fetch that throws shows the
// rejection costs neither an upstream call nor a rate-limit slot.
const request = install(installRouteMiddleware);
t.mock.method(globalThis, 'fetch', async () => {
throw new Error('upstream must not be reached');
});
for (const method of ['POST', 'PUT', 'DELETE', 'PATCH']) {
const response = await request(
'/api/route',
'?profile=car&coords=-97,30;-97.01,30.01',
method,
);
assert.equal(response.statusCode, 405, method);
assert.deepEqual(response.body, { error: 'Method Not Allowed' });
}
});