A Retry-After longer than the 10 s retry budget used to sleep the whole
budget out and then give up, so the proxy's 429 pause started 10 s late
and every new-point poll in that window still went upstream and logged.
The retry loop now stops at once in that case.
Tests: the retry loop no longer sleeps for an unreachable Retry-After;
fully cached points, including stale ones, are served during the pause
with no upstream call; a 503 does not pause; a shorter concurrent
Retry-After never cuts the pause short. CURRENT-STATE now says which
requests are still served and which log.
Review follow-ups: the upstream error now carries its HTTP status and the
pause checks error.status instead of the message text; concurrent 429s
extend the pause with Math.max rather than a shorter Retry-After cutting a
longer one short; the per-chunk re-check says it is for multi-chunk
requests; the first 429 test runs its backoff on mocked timers.
CURRENT-STATE describes the pause.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A rate-limited upstream got asked again by every page request for new
points (about once a second while the camera moves), each with its own
retries, which kept the limit in force and logged "refresh incomplete" each
time. After a 429 the proxy now leaves the upstream alone for its
Retry-After (at most 10 minutes), or 60 s when it sends none, serving
cached points meanwhile. The warning that starts the pause says
"(HTTP 429; pausing upstream for N s)"; requests refused during the pause
don't log.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Once the cache was full, every write sorted all 50,000 entries to find the
oldest. A Map keeps insertion order, so keep the map in fetch-time order
and delete from the front: the resolver re-inserts each point it refetches
(an overwrite would leave it in its old place), and the disk load sorts
once, since a file written before this change, or a migrated raw-batch
cache, is not in age order.
Suggested by asturksever on #975.
Review follow-up on #975. The shared same-site gate refuses any forwarding header, which is right for routes that spend a key, but terrain is keyless and needed for rendering: behind the reverse proxy the README documents, every /api/terrain/heights chunk got 403 and fell back to the geoid. Terrain now uses admitSameSiteKeyless, which keeps the Sec-Fetch-Site and exact-Origin checks (comparing Origin against a single X-Forwarded-Proto scheme) but does not refuse forwarding headers. The shared admitSameSite gate is unchanged for every other route. The changelog entry moves to changelog.d/975-terrain-cross-site-gate.md.
Co-authored-by: daikaginza <[email protected]>
Follow-up to #686 requested on #923. The route's per-client limiter keys on the socket address, which for a cross-site <img> is the visitor's own browser, so any web page could fill the bounded cache with junk keys and spend the visitor's 90/min quota, leaving the app's own terrain lookups on 429. Gate the route with admitSameSite before the limiter, like the Places routes.
Co-authored-by: daikaginza <[email protected]>
Every distinct 5-decimal point was a permanent in-memory entry, a
permanent line in an uncapped `.gev-cache/terrain-heights.json`, and a
live upstream call. Nothing limited the rate, and coordinates were
checked for being finite rather than for being on Earth, so an
out-of-range pair was still a valid cache key.
Four bounds, each following a pattern this repo already uses:
- WGS-84 range is enforced in `parseTerrainPoints`, at the edge, so no
later stage has to. Poles and the antimeridian still parse.
- The point cache holds at most 50,000 entries and evicts oldest-first,
the shape `trackBackfillProxies` uses. Sized from measurement: this
checkout's cache file held 6,189 entries in 1.0 MB of real use, so the
ceiling is eight times that working set. Terrain does not move, so an
evicted point costs one refetch and never a wrong answer.
- A cache file above 24 MB is not read back, mirroring
`rocketLaunchesProxy`. An unmeasurable file is not treated as an
oversized one: without stat, readFile decides, exactly as before.
- The route carries an always-on limiter of 90/min per client, 270
global — the `/api/overpass` band, and an order of magnitude above
measured demand (a full test:track run drove 10 requests, peak 8/min).
The ceilings are constructor options so they can be exercised as
behaviour rather than asserted as constants; production passes none.
They are destructured in the body because `proxyErrorResponses.test.mjs`
slices these proxies to the first column-zero closing brace.
Use 64-point browser and upstream terrain batches to reduce cold-cache timeouts. Preserve successful chunk results, distinguish absent readings from malformed responses, and reject fabricated zero heights.
Original terrain resilience contribution by ItzDESTAQ, with browser batching, validation and regression follow-up by Sameh Khamis.
Validated with unit/build/format/boundary checks, 108 tracking checks, 5 terrain-outage checks, controlled slow-upstream comparison and live browser checks.
Co-authored-by: ItzDESTAQ <[email protected]>
Co-authored-by: Sameh Khamis <[email protected]>