The access token rotates every half hour and lives in the query string, so it
is part of the browser's cache key: every rotation orphans every cached tile,
and the week-long max-age core sends is worth nothing. The service worker works
around it by stripping the token from its own cache key, but a service worker
only runs on https or localhost - on a plain http LAN origin, which is how a
lot of instances and the companion apps are reached, there is no cache at all
between the map and the network.
Anything that can set a header sends the token there instead, which leaves the
URL, and with it the cache key, stable across rotations. Raster tiles come from
an <img> and stay on the query parameter, and so does Cast: cross-origin a
custom header would cost a CORS preflight per tile.
Needs the matching core change; against a core without it the proxy refuses
every tile.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Core now proxies both vector and raster tiles, which is what lets them be
requested with an application User-Agent and without a referrer - a
browser can set neither. So the frontend stops talking to OpenStreetMap
and CARTO directly and goes through /api/map_tiles.
The proxy is token gated, so `ha-map` fetches one over the WebSocket
before setting up, following the brands token pattern: cached at module
level, refreshed well inside its lifetime so a dashboard left open for
days keeps working. The blocking wait is kept to about a second - a
backend without the proxy must not hold the map hostage - and the
remaining retries run in the background to ride through the window after
a restart where the WebSocket is up but the handler is not registered
yet.
Two things that are not obvious and cost a measurement each:
MapLibre's `transformRequest` has to return absolute URLs. Tiles are
fetched from a worker, which has no document to resolve a relative URL
against, and the TileJSON that core serves has relative `tiles`. Measured
with a relative TileJSON on one origin: with absolute URLs the style
loads and 8,456 features render; without them exactly one request is made
- the style - and nothing else loads, with no error reported anywhere.
Leaflet bakes its URL template at layer creation and throws while
building a tile URL if a template variable is undefined. So the raster
layer takes the token as an option Leaflet substitutes per request, which
also means a refreshed token is picked up without recreating the layer,
and an absent one is empty rather than missing: the tiles 403 and the
markers still draw.
The asset pipeline shrinks to generating two styles. Glyphs and sprites
come from the proxy, so the 48 MB build-time download, the digest
verification, the glyph range filtering and the bold-range guard are all
gone, along with 5.4 MB from the wheel. `localIdeographFontFamily` goes
too: the complete glyph set is reachable now, so CJK renders in Noto
rather than a device font.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>