mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
fix(server): stop relaying upstream and JS error text to the client
Ported onto the split provider layout: the routes now live in
server/providers/openai/{hud-summary,realtime}.js and the camera health
entry in server/providers/cctv.js.
The routes #263 names — celestrak, terrain, adsbdb — were sanitized by
#289. The same defect had moved:
- /api/openai/hud-summary relayed `data.error.message` verbatim on a
non-ok upstream. That is OpenAI's own error text, carrying request ids,
organization hints and quota wording. Its catch echoed the JS error too.
- /api/realtime/token echoed the JS error when minting failed, which for a
network fault is a resolver message naming the upstream host.
- cctvProxy stored the raw error as a camera's health `message`. This is
the least obvious of the three and the reason the PR is worth its size:
GET /api/cctv/health serializes `message`, and src/layers/cctv/frames.js
renders it as the camera's status label, so the detail reached the
screen through a different door than the sanitized 502 beside it.
Logs name the failure and the upstream status without the text, matching
the Launch Library and terrain paths; these lines get pasted into issues.
Coverage extends the existing harnesses rather than adding files —
localServices.test.mjs drives both OpenAI routes over a mocked upstream,
mediaProviders.test.mjs drives a failing media fetch and then reads
/health. All three assertions fail on the previous implementation.
Refs #263. Does not touch /api/realtime/debug-log's identical echo; that
route is the subject of the #23 PR, so the two do not overlap a line.
This commit is contained in:
1 parent
96a6b49477
commit
9e71081a5a
7 files changed
+97
-8
No files matched your search
@@ -1,5 +1,7 @@
|
||||
# Changelog
|
||||
|
||||
- Three proxy paths no longer relay upstream or JS error text to the client. The HUD summary passed OpenAI's own `error.message` through whenever upstream was not ok, carrying request ids and quota wording; the Realtime token route echoed the JS error, which for a network fault names the upstream host; and a failed CCTV media fetch stored the raw errno as the camera's health message, which reaches the screen through `GET /api/cctv/health` rather than through the sanitized response beside it. Logs now name the failure and the upstream status without the text.
|
||||
|
||||
- Separate vessel records and feed acquisition from rendering while preserving selection, partial-feed retention, sea-surface placement and request cancellation.
|
||||
|
||||
- Separate military-flight records and acquisition from rendering while preserving ground-model ownership, source units and follow behavior.
|
||||
|
||||
@@ -3192,6 +3192,7 @@ are omitted rather than framing the wrong part of the globe.
|
||||
- A cold OpenSky failure uses the current camera subpoint only to request a cached adsb.lol point fallback capped at 250 nm. A fresh OpenSky response or last-good cache wins; a nominally successful worldwide snapshot more than two minutes old prefers viewport-scoped adsb.lol when available, otherwise the stale source is reported honestly. The fallback is visibly source-labeled and is never presented as a worldwide snapshot.
|
||||
- GBFS proxy refuses upstream redirects (`redirect: 'manual'`; any 3xx becomes a 502 and the redirect target is logged server-side only) and enforces its 5 MB response cap while the body streams, cancelling the upstream read past the cap; CCTV health map is bounded.
|
||||
- Proxy error payloads are sanitized (no internal error details returned to clients).
|
||||
- That holds for the OpenAI and CCTV media paths too: `/api/openai/hud-summary` never relays OpenAI's own `error.message`, `/api/realtime/token` answers a fixed string when minting fails, and a failed CCTV media fetch stores a fixed camera health `message` — `GET /api/cctv/health` serializes that field and the CCTV panel renders it as a status label, so it is a client surface as much as the response body is.
|
||||
- `OPENAI_API_KEY` is server-side only; the browser receives ephemeral Realtime client secrets from `/api/realtime/token`.
|
||||
- `AISSTREAM_API_KEY` is server-side only; the browser reads the same-origin `/api/ais-live` cache.
|
||||
- `/api/google/nearby-places` keeps the Google key out of Places requests issued for voice scene context.
|
||||
|
||||
@@ -308,11 +308,16 @@ export function cctvProxy({ sourceRoot = process.cwd() } = {}) {
|
||||
}
|
||||
const timedOut =
|
||||
error?.name === 'AbortError' || error?.name === 'TimeoutError';
|
||||
// GET /api/cctv/health serializes `message`, and the CCTV panel
|
||||
// renders it as a status label, so the raw error would leave the
|
||||
// server by a different door than the sanitized body below and
|
||||
// land on screen. Keep the detail in the log.
|
||||
console.warn('[CCTV Proxy] media fetch failed');
|
||||
setHealth(cameraId, {
|
||||
status: 'degraded',
|
||||
sourceKind: 'upstream',
|
||||
label: source?.provider || 'Configured source',
|
||||
message: error?.message || 'Media fetch failed',
|
||||
message: 'Media fetch failed',
|
||||
});
|
||||
res.writeHead(timedOut ? 504 : 502, {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -78,20 +78,23 @@ async function handleHudSummary(req, res) {
|
||||
res.statusCode = response.ok && summary ? 200 : response.status || 502;
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
if (!response.ok)
|
||||
console.warn(`[hud-summary] upstream HTTP ${response.status}`);
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
summary: summary || null,
|
||||
error: response.ok
|
||||
? null
|
||||
: data.error?.message || 'OpenAI HUD summary request failed',
|
||||
// Never relay `data.error.message`: that is OpenAI's own wording, and
|
||||
// it carries request ids, organization hints and quota phrasing.
|
||||
error: response.ok ? null : 'OpenAI HUD summary request failed',
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
console.warn('[hud-summary] request failed');
|
||||
res.statusCode = 502;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
error: error?.message || 'OpenAI HUD summary request failed',
|
||||
error: 'OpenAI HUD summary request failed',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,12 +146,15 @@ function createRealtimeTokenHandler({
|
||||
res.setHeader('X-GEV-Voice-Tier-Fallback', '1');
|
||||
}
|
||||
res.end(body);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// For a network fault this was a resolver message naming the upstream
|
||||
// host; the client only needs to know the mint failed.
|
||||
console.warn('[realtime-token] mint failed');
|
||||
res.statusCode = 502;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
error: error?.message || 'Failed to create Realtime token',
|
||||
error: 'Failed to create Realtime token',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -283,3 +283,36 @@ test('Realtime service configuration selects compatible endpoint/model without f
|
||||
assert.equal(response.headers['cache-control'], 'no-store');
|
||||
assert.doesNotMatch(response.body, /server-fixture|voice\.example/);
|
||||
});
|
||||
|
||||
test('OpenAI routes answer generically when the upstream or the request fails', async (t) => {
|
||||
env(t, 'OPENAI_API_KEY', 'fixture-upstream-secret');
|
||||
env(t, 'GEV_RATELIMIT_OPENAI_PER_MIN', undefined);
|
||||
const leak =
|
||||
'fixture-upstream-secret req_fixture_1234 org-fixture quota exhausted';
|
||||
|
||||
// `data.error.message` is OpenAI's own wording — request ids, organization
|
||||
// hints, quota phrasing — and was relayed verbatim whenever upstream was not ok.
|
||||
t.mock.method(globalThis, 'fetch', async () =>
|
||||
Response.json({ error: { message: leak } }, { status: 429 }),
|
||||
);
|
||||
const summary = await request(
|
||||
install(openAiRealtimeProxy()).get('/api/openai/hud-summary'),
|
||||
{ method: 'POST', body: JSON.stringify({ context: {} }) },
|
||||
);
|
||||
assert.equal(summary.json().error, 'OpenAI HUD summary request failed');
|
||||
assert.equal(summary.body.includes('req_fixture_1234'), false);
|
||||
assert.equal(summary.body.includes('fixture-upstream-secret'), false);
|
||||
|
||||
// A network fault surfaced a resolver message naming the upstream host.
|
||||
t.mock.restoreAll();
|
||||
t.mock.method(globalThis, 'fetch', async () => {
|
||||
throw Error(`getaddrinfo ENOTFOUND api.openai.com ${leak}`);
|
||||
});
|
||||
const token = await request(
|
||||
install(openAiRealtimeProxy()).get('/api/realtime/token'),
|
||||
);
|
||||
assert.equal(token.status, 502);
|
||||
assert.deepEqual(token.json(), { error: 'Failed to create Realtime token' });
|
||||
assert.equal(token.body.includes('api.openai.com'), false);
|
||||
assert.equal(token.body.includes('fixture-upstream-secret'), false);
|
||||
});
|
||||
@@ -149,3 +149,45 @@ for (const hook of ['configureServer', 'configurePreviewServer']) {
|
||||
assert.equal(cancelled, true);
|
||||
});
|
||||
}
|
||||
|
||||
test('a failed CCTV media fetch reports a fixed health message, not the error text', async (t) => {
|
||||
isolate(t);
|
||||
const root = mkdtempSync(path.join(tmpdir(), 'gev-cctv-'));
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
mkdirSync(path.join(root, 'config'));
|
||||
writeFileSync(
|
||||
path.join(root, 'config/cctv_sources.austin.json'),
|
||||
JSON.stringify([
|
||||
{
|
||||
id: 'leaky',
|
||||
name: 'Leaky camera',
|
||||
lat: 30.27,
|
||||
lon: -97.74,
|
||||
feedType: 'video',
|
||||
url: 'https://cams.fixture.invalid/leaky.m3u8',
|
||||
},
|
||||
]),
|
||||
);
|
||||
const leak =
|
||||
'connect ETIMEDOUT 203.0.113.9:443 C:\fixture\secret-path\cams.json';
|
||||
t.mock.restoreAll();
|
||||
t.mock.method(globalThis, 'fetch', () => {
|
||||
throw Error(leak);
|
||||
});
|
||||
|
||||
const call = install(cctvProxy({ sourceRoot: root }));
|
||||
const media = await call('/media/leaky');
|
||||
assert.equal(media.status, 502);
|
||||
assert.equal(media.body.includes('203.0.113.9'), false);
|
||||
|
||||
// GET /health is the second door: src/layers/cctv/frames.js renders each
|
||||
// entry's `message` as the camera's status label, so a raw errno stored here
|
||||
// reaches the screen even though the response above is sanitized.
|
||||
const camera = JSON.parse((await call('/health')).body).cameras.find(
|
||||
(entry) => entry.id === 'leaky',
|
||||
);
|
||||
assert.equal(camera.status, 'degraded');
|
||||
assert.equal(camera.message, 'Media fetch failed');
|
||||
assert.equal(camera.message.includes('ETIMEDOUT'), false);
|
||||
assert.equal(camera.message.includes('secret-path'), false);
|
||||
});
|
||||
Reference in new issue
Block a user