The comment claimed "Keep the detail in the log" while the line it introduces
carries none, so the next reader would have read the omission as an oversight
and added the errno back.
Detail-free is the deliberate choice: a media-fetch failure names the camera's
upstream host, and these lines get pasted into issues. Say that, and say why
the catch at the bottom of this file may still log its error text.
The in-memory cache in the terrain-height resolver had no eviction, so a
session retained every coordinate it ever resolved for the lifetime of the
page. The 5-decimal rounding documented in the file header makes repeated
visits to one place share a key, but it does not bound how many distinct
places a session visits.
Cap it at 20 000 entries, overridable per instance so the bound is testable
without minting twenty thousand coordinates. Eviction is least-recently-used
rather than insertion-ordered because revisits here are spatial: the cell the
camera is parked on must not be dropped for having been resolved early.
The bound sits far above a city-scale session at the ~111 m floor grid, so
normal use never evicts and issues no additional proxy requests. Batch results
are assembled from what the call resolved, so eviction during a large batch
cannot report a just-resolved point as unresolved.
Ported onto the split provider layout: the route now lives in
server/providers/openai/debug-log.js and its state belongs to the handler
the factory returns rather than to module scope.
/api/realtime/debug-log is the one route in its family left unhardened.
Both siblings enforce a limiter through openai/rate-limit.js, and this was
the only handler still writing with fs.appendFileSync on the request path.
- Always-on per-IP limiter (120/min, 400 global). Deliberately not the
opt-in openAiRateLimiter(): that one defaults to unlimited so throttling
cannot silently change what a user spends, and this sink spends disk and
event-loop time rather than provider quota.
- Serialized async appends with a 32 MB rotation ceiling and one retained
generation. REALTIME_DEBUG_LOG_MAX_BYTES caps a single request body; it
never bounded the file those bodies accumulate into, so any local page
could grow .gev-logs/realtime-conversations.jsonl for as long as the dev
server ran. Serializing also closes a read-then-write race on the size
check, where concurrent callers all stat under the ceiling and none
rotates.
- Generic 400/500 answers separating a malformed record or oversized body
from a failed write, instead of echoing error.message — an errno and an
absolute path for an fs failure.
- readRequestBody marks an oversized body with code 'BODY_TOO_LARGE', the
marker its sibling readRequestBodyCapped already sets, so the two cases
are distinguishable without matching on text.
Coverage extends src/tooling/localServices.test.mjs, which already drives
the real handler over a temporary sourceRoot. Both new tests fail on the
previous implementation and pass with this one.
Refs #23. Does not add authentication: these proxies have no auth
mechanism today, so introducing one is a maintainer design decision.
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.