fix(geoloc): cap the bounding box area on GET /geoloc/entrances - #1813
Merged
Merged
Conversation
The POST /massifs 400 description still cited 8 000 km², the value in force before d33eb56 raised MassifService.MAX_AREA_KM2 to 35 000. PUT /massifs/{id} was already correct.
An unbounded bounding box matched ~134 000 entrances. Postgres returned that set in ~1.3s, but the endpoint took 5-40s: the remaining time was Node marshalling and serialising 134k rows x 25 columns synchronously on a single event loop, so every other request queued behind it. Bucketing unrelated endpoints by concurrent large geoloc requests showed p95 rising from 713ms to 4441ms with medians flat - head-of-line blocking, not database contention. Clients then gave up, and App Service counted the aborted connections toward Http5xx, firing the Sev1 alert. - Reject a bounding box over 35 000 km² with 400 BBOX_AREA_EXCEEDED - Compute the area in JS, not PostGIS: ST_Area(::geography) raises "Antipodal edge detected" on world bounds, the exact input to reject - Use absolute, non-wrap-aware deltas, matching ST_MakeEnvelope's min/max normalisation and the planar ST_Within predicate the query uses; an inverted longitude covers the complement, not a 20° strip - Check before the massif lookup, so an oversized box never pays for a database round trip before being rejected - Keep the cap out of checkAndGetCoordinatesParams: all eight geoloc controllers share it, and four other endpoints serve world bounds to the map on every page load - Shrink three route-test bounding boxes that exceeded the new limit Closes #1810
Paul-AUB
approved these changes
Sep 22, 2026
Paul-AUB
left a comment
Contributor
There was a problem hiding this comment.
Reviewed the critical request path, bounding-box calculation, API contract, and focused test coverage. No must-fix issues found.
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rejects bounding boxes larger than 35 000 km² on
GET /api/v1/geoloc/entranceswith a400 BBOX_AREA_EXCEEDED, before any query runs.Why
numberOfWorkers = 1).Http5xxplatform metric. 193 aborts in 4 days, 86 on this endpoint, while the application returned zero 500s.rateLimiter.jsallows 200 requests per 10 minutes per IP; the observed 3–5 world-scale requests per minute sits far inside that budget.grottocenter-front: the web map fetches one z12 tile per request (67–95 km², ~370–520× headroom),MapMassifis gated on zoom ≥ 13 (~1 300 km²), and duplicate detection uses a 1 km radius (~4 km²). Server-side,grottocenter.org(12 530 requests) and the mobile app (1 224) have zero requests over the cap.What
api/utils/computeBoundingBoxAreaKm2.js— exact spherical rectangle area, pure and synchronous.GeoLocService.validateBoundingBoxArea()— returnsnull | { code, message }, mirroringMassifService.validatePolygon, and logs onewarnon rejection (log-response.jswould otherwise record a 400 only atverbose).find-entrances.js, placed before the massif lookup so an oversized box never pays for a database round trip first. Consequence: an oversized box plus an unknownmassifid now returns 400 rather than 404 — pinned by a test.400response, and a note that inverted longitudes are normalised rather than wrapped.Two details worth a reviewer's attention:
The cap is deliberately not in
checkAndGetCoordinatesParams. That function is the first statement of all eight geoloc controllers, and the web app requests world bounds from four of the others on every map page load. Putting the check there would break the production map.The area is not wrap-aware, on purpose. The query filters with planar
ST_Within(point_geom, ST_MakeEnvelope(...)). Verified on PostGIS 3.4.3:ST_MakeEnvelope(170,-10,-170,10,4326)yieldsPOLYGON((170 -10,170 10,-170 10,-170 -10,170 -10)), andST_WithinacceptsPOINT(0 0)while rejectingPOINT(175 0)— a 340°-wide box of ~83.6M km², not a 20° strip. A wrap-awareΔλwould understate the most expensive requests by 17× and let them through. The area is also in km² rather than degrees², because the polaruseNearbyEntrancesbox is 6.5 degrees² but only ~3 km² of real surface and must keep working.Related
POST /massifsstill cited the pre-d33eb56blimit); it is separated so it can be read on its own.Follow-ups, not in this PR
MapMassif.jsxcalls.wrap()per corner, so panning a massif map across ±180 produces|Δλ| ≈ 359.6°→ 400. Degrades gracefully (markers vanish, no crash) and needs a massif straddling the antimeridian. Frontend issue.?sw_lat=coerces to 0. Affects all eight geoloc endpoints.work_memsort spill onPUBLIC_ENTRANCES_IN_BOUNDS— this cap shrinks the sort input and may remove it without server tuning.