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:
Pedro Lobato committed 2026-09-14 21:24:05 -05:00
1 parent 96a6b49477
commit 9e71081a5a
7 files changed
+97 -8

No files matched your search

+33
View File
@@ -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);
});
+42
View File
@@ -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);
});