Skip to content

Commit 0eee054

Browse files
authored
feat: re-instate hex tileset metadata.json tile-host route (un-revert #287) (#316) (#317)
Un-reverts #287, restoring the GET /tiles/{namespace}/{name}/metadata.json route added in #255. The transcription-leak defense the route provides is no longer a one-time qwen3 event (z-ai/glm-5.2 leaked its XML arg-dialect into value_stats on ca-30x30, 2026-07-14), and geo-agent#276 now makes the client fetch value_stats/bounds by hash from this route instead of routing the JSON through the LLM's tool-call args -- resolving the 'no consumer' objection. Restores serve_metadata, the route mount, and the TestServeMetadata suite.
1 parent 62fb312 commit 0eee054

3 files changed

Lines changed: 78 additions & 3 deletions

File tree

server.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ async def _query_tool(
382382
import concurrent.futures
383383
import threading
384384
import time
385-
from tiles.endpoint import serve_tile
385+
from tiles.endpoint import serve_metadata, serve_tile
386386
from tiles.db import build_tile_connection
387387
from tiles.pyramid import (
388388
MVT_LAYER_NAME,
@@ -869,6 +869,9 @@ def mount_tiles(app):
869869
con = _get_tile_con()
870870
app.state.tile_con = con
871871
app.add_route("/tiles/{namespace}/{name}/{z:int}/{x:int}/{y:int}.pbf", serve_tile)
872+
# Same-origin metadata sidecar so clients can read value_stats/bounds by
873+
# hash instead of routing the large JSON through an LLM's tool-call args.
874+
app.add_route("/tiles/{namespace}/{name}/metadata.json", serve_metadata, methods=["GET"])
872875

873876
# -------------------------------------------------------------------------
874877
# 9. OPTIONAL BEARER TOKEN AUTH

tests/test_tile_endpoint.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from starlette.routing import Route
66
from starlette.testclient import TestClient
77

8-
from tiles.endpoint import serve_tile
8+
from tiles.endpoint import serve_metadata, serve_tile
99
from tiles.pyramid import register_hex_tiles
1010
from tiles.db import build_tile_connection
1111

@@ -25,6 +25,7 @@ def app_with_tiles(local_bucket):
2525
con = build_tile_connection()
2626
app = Starlette(routes=[
2727
Route("/tiles/{namespace}/{name}/{z:int}/{x:int}/{y:int}.pbf", serve_tile),
28+
Route("/tiles/{namespace}/{name}/metadata.json", serve_metadata, methods=["GET"]),
2829
])
2930
app.state.tile_con = con
3031
yield app
@@ -310,3 +311,48 @@ def test_endpoint_reads_finest_res_from_metadata(self, app_with_tiles, local_buc
310311
y = int((1 - math.log(math.tan(lat_rad) + 1/math.cos(lat_rad)) / math.pi) / 2 * n)
311312
r = client.get(f"/tiles/hex/{result['hash']}/{z}/{x}/{y}.pbf")
312313
assert r.status_code in (200, 204) # not 404
314+
315+
316+
class TestServeMetadata:
317+
"""The /tiles/hex/<hash>/metadata.json sidecar route. Clients fetch
318+
value_stats/bounds by hash here instead of transcribing them through an
319+
LLM's tool-call args (weak models corrupt the large JSON → silent
320+
no-op layer-add). See the hex-not-showing investigation."""
321+
322+
def test_returns_metadata_json_with_value_stats_and_bounds(self, app_with_tiles, registered_ca):
323+
client = TestClient(app_with_tiles)
324+
r = client.get(f"/tiles/hex/{registered_ca}/metadata.json")
325+
assert r.status_code == 200
326+
assert r.headers["content-type"].startswith("application/json")
327+
meta = r.json()
328+
# The exact fields the client needs to build the color scale + fit view.
329+
assert "value_stats" in meta
330+
assert "bounds" in meta and len(meta["bounds"]) == 4
331+
assert meta["finest_res"] == 5
332+
assert "layer_name" in meta
333+
334+
def test_immutable_cache_control(self, app_with_tiles, registered_ca):
335+
client = TestClient(app_with_tiles)
336+
r = client.get(f"/tiles/hex/{registered_ca}/metadata.json")
337+
assert r.status_code == 200
338+
cc = r.headers.get("cache-control", "")
339+
assert "immutable" in cc and "public" in cc
340+
341+
def test_unknown_hash_returns_404(self, app_with_tiles):
342+
client = TestClient(app_with_tiles)
343+
r = client.get("/tiles/hex/deadbeefdeadbeef/metadata.json")
344+
assert r.status_code == 404
345+
346+
def test_non_hex_namespace_returns_404(self, app_with_tiles, registered_ca):
347+
client = TestClient(app_with_tiles)
348+
r = client.get(f"/tiles/nothex/{registered_ca}/metadata.json")
349+
assert r.status_code == 404
350+
351+
def test_metadata_url_derives_from_tile_url_suffix_swap(self, app_with_tiles, registered_ca):
352+
"""The contract the client relies on: swapping the tile_url's
353+
`/{z}/{x}/{y}.pbf` suffix for `metadata.json` hits this route."""
354+
client = TestClient(app_with_tiles)
355+
tile_url = f"/tiles/hex/{registered_ca}/5/5/12.pbf"
356+
meta_url = tile_url.rsplit("/", 3)[0] + "/metadata.json"
357+
assert meta_url == f"/tiles/hex/{registered_ca}/metadata.json"
358+
assert client.get(meta_url).status_code == 200

tiles/endpoint.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import time
1616
import anyio
1717
from starlette.requests import Request
18-
from starlette.responses import Response
18+
from starlette.responses import JSONResponse, Response
1919

2020
from tiles.tile_math import (
2121
h3_edge_padding_deg,
@@ -267,6 +267,32 @@ def _run_tile_query(con, sql: str) -> bytes:
267267
return bytes(row[0])
268268

269269

270+
async def serve_metadata(request: Request) -> Response:
271+
"""GET /tiles/{namespace}/{name}/metadata.json
272+
273+
Serve the tileset's metadata sidecar (value_stats, bounds, layer_name,
274+
value_columns, finest_res, ...) from the same origin as the tiles. The
275+
client derives this URL from the tile_url template by swapping the
276+
`/{z}/{x}/{y}.pbf` suffix for `metadata.json`, then reads the color-scale
277+
inputs directly — so they never have to be transcribed through an LLM's
278+
tool-call arguments (where weak models corrupt the large value_stats JSON,
279+
silently dropping the layer-add; see the hex-not-showing investigation).
280+
"""
281+
namespace = request.path_params["namespace"]
282+
name = request.path_params["name"]
283+
284+
if namespace != "hex":
285+
return Response(status_code=404)
286+
287+
con = request.app.state.tile_con
288+
meta = await anyio.to_thread.run_sync(
289+
_get_cached_metadata, request.app.state, con, namespace, name
290+
)
291+
if meta is None:
292+
return Response(status_code=404)
293+
return JSONResponse(meta, headers={"Cache-Control": TILE_CACHE_CONTROL})
294+
295+
270296
async def serve_tile(request: Request) -> Response:
271297
"""GET /tiles/{namespace}/{name}/{z}/{x}/{y}.pbf"""
272298
namespace = request.path_params["namespace"]

0 commit comments

Comments
 (0)