fix(server): declare Allow: GET on the new 405 responses

RFC 9110 section 15.5.6 requires a 405 to name the methods it accepts, and
/api/radio/stations already does this in-tree. Thanks to kvnloo for catching it
on review. The same gap on the pre-existing handlers is tracked in #809 and is
deliberately left alone here.
This commit is contained in:
daikaginza committed 2026-09-29 10:56:20 +07:00
1 parent fb6b4e34f0
commit 2e5dfce1af
6 files changed
+15 -4

No files matched your search

+1 -1
View File
@@ -4072,7 +4072,7 @@ are omitted rather than framing the wrong part of the globe.
- `/api/overpass` is bounded by body/response caps, per-client/global rate limits, concurrency limits, operator-configured upstreams, in-flight dedupe, cache bounds, and static validation that every selector is spatially bounded.
- `/api/military-installations` uses an independent limiter with the same 90-per-client/300-global one-minute bounds, so viewport installation refreshes never consume `/api/overpass` annotation capacity.
- `/api/route` proxies bounded OSRM route requests for annotation routes, with profile allowlisting, distance caps, response caps, caching, and sanitized "no route found" errors.
- `/api/opensky`, `/api/adsblol/mil`, and `/api/route` are read-only query proxies and answer `405 Method Not Allowed` to anything but `GET`, as `/api/geocode` and the other query proxies already did. The refusal precedes rate limiting and any upstream call.
- `/api/opensky`, `/api/adsblol/mil`, and `/api/route` are read-only query proxies and answer `405 Method Not Allowed` to anything but `GET`, as `/api/geocode` and the other query proxies already did, declaring `Allow: GET` with it as `/api/radio/stations` does. The refusal precedes rate limiting and any upstream call.
- Track endpoints: `/api/ais-live/track?mmsi=` (server-accumulated ring buffers; sub-route handled before the rows snapshot), `/api/opensky-track?icao24=` (OAuth, 60s cache, sanitized errors, independent OpenSky credit bucket), `/api/adsblol/trace?hex=` (60s cache, 5MB cap, ODbL attribution required in UI).
- Realtime debug logs redact API keys, bearer tokens, client secrets, and image data URLs before writing to disk; request bodies are size-capped.
- `/api/realtime/debug-log` enforces an always-on 120-per-client/400-global one-minute limiter (not the opt-in `GEV_RATELIMIT_OPENAI_PER_MIN` bucket the cost-bearing OpenAI routes share), appends asynchronously through a serialized queue, and rotates `realtime-conversations.jsonl` at 32 MB keeping one prior generation, so the sink is bounded at twice that regardless of session length. A malformed record answers 400 and a failed write 500, both with a fixed message.
+4 -1
View File
@@ -71,7 +71,10 @@ export function adsbLolProxy() {
// Read-only snapshot endpoint; match the method contract its sibling
// query proxies already declare instead of serving every verb alike.
if (req.method !== 'GET') {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.writeHead(405, {
'Content-Type': 'application/json',
Allow: 'GET',
});
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
return;
}
+4 -1
View File
@@ -354,7 +354,10 @@ export function openSkyProxy() {
// /api/geocode and the other query proxies state it, rather than
// answering any verb with the same body and ignoring the request body.
if (req.method !== 'GET') {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.writeHead(405, {
'Content-Type': 'application/json',
Allow: 'GET',
});
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
return;
}
+1 -1
View File
@@ -230,7 +230,7 @@ export function installRouteMiddleware(
// The documented shape above is a GET with query parameters; say so,
// the way the other query proxies do, instead of answering any verb.
if (req.method !== 'GET') {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.writeHead(405, { 'Content-Type': 'application/json', Allow: 'GET' });
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
return;
}
+3
View File
@@ -398,6 +398,9 @@ test('the aircraft snapshot routes decline a non-GET before any upstream work',
for (const method of ['POST', 'PUT', 'DELETE', 'PATCH']) {
const response = await request(route, '/', method);
assert.equal(response.statusCode, 405, `${method} ${route}`);
// RFC 9110 section 15.5.6 requires a 405 to name the methods it does
// accept, which is what `/api/radio/stations` already does in-tree.
assert.equal(response.headers.allow, 'GET', `${method} ${route}`);
assert.deepEqual(JSON.parse(response.body), {
error: 'Method Not Allowed',
});
+2
View File
@@ -498,6 +498,8 @@ test('the routing proxy declines a non-GET before rate limiting or upstream', as
method,
);
assert.equal(response.statusCode, 405, method);
// RFC 9110 section 15.5.6 requires a 405 to name the methods it accepts.
assert.equal(response.headers.allow, 'GET', method);
assert.deepEqual(response.body, { error: 'Method Not Allowed' });
}
});