Merge pull request #935 from daikaginza/fix/tile-origin-follows-tilejson

fix(tiles): derive the tile origin from the configured TileJSON (#933)
This commit is contained in:
Sameh Khamis authored and GitHub committed 2026-10-08 10:52:10 -07:00
commit 06b5ac7d12
6 files changed
+115 -3

No files matched your search

+5
View File
@@ -1,5 +1,10 @@
# Changelog
- Vector tile sources take their allowed tile origin from the configured
`tileJsonUrl`, so repointing only that URL at a mirror loads tiles from the
mirror or fails visibly instead of silently using OpenFreeMap. Thanks to
[daikaginza](https://github.com/daikaginza) (#935).
- MCP setup examples use the app's default port, `4173`. Thanks to
[Huanyi Xie](https://github.com/xiehuanyi) (#934) and
[shirunjies8-png](https://github.com/shirunjies8-png) (#959) for the same fix.
+4
View File
@@ -4704,6 +4704,10 @@ easier to meet (detection is now on more often), but does not create it.
shown dot against the allowed source classes and records phase timings.
Positions and segment distances are precomputed, with no new animation-loop
allocation. Visible-globe roads use cached terrain without offscreen mesh picks. The road-source label names the geometry being drawn, with partial/unavailable states shown plainly.
- Vector tile sources accept tiles only from an allowed origin, which defaults
to the configured `tileJsonUrl` origin; an explicit `allowedOrigin` overrides
it. Repointing only `tileJsonUrl` loads tiles from that host or fails with an
origin error rather than silently falling back to OpenFreeMap.
- TileJSON caches successful metadata. Transient failures retry after a
five-second cooldown; invalid metadata/origins stay unavailable until the
source is cleared. Clear resets metadata and cancels pending requests.
+9 -2
View File
@@ -214,11 +214,18 @@ function decodeLayers(bytes, z, x, y, layerNames) {
return { roads, military };
}
/** Construct an immutable-version road/military tile source without starting I/O. */
/**
* Construct an immutable-version road/military tile source without starting I/O.
*
* `allowedOrigin` is deliberately not set here: `createVectorTileSource`
* derives it from whichever `tileJsonUrl` is in effect, so a caller that
* repoints only `tileJsonUrl` gets tiles from that host or a named error —
* never a silent fall back to the default host (#933). Passing
* `allowedOrigin` explicitly still overrides both.
*/
export function createOpenFreeMapSource(options = {}) {
return createVectorTileSource({
tileJsonUrl: 'https://tiles.openfreemap.org/planet',
allowedOrigin: 'https://tiles.openfreemap.org',
decode: decodeOpenFreeMapTile,
maxEntries: 192,
maxCacheBytes: 64 * 1024 * 1024,
+38
View File
@@ -320,3 +320,41 @@ test('Capitol, greenbelt and commercial property fixtures exclude non-public dri
assert.ok(records.some((r) => r.area === area && !r.drivable));
}
});
test('repointing tileJsonUrl at a mirror never falls back to the default tile host', async () => {
const urls = [];
const mirror = 'http://127.0.0.1:12348';
const source = createOpenFreeMapSource({
fetchImpl: async (url) => {
urls.push(url);
return url.endsWith('/planet')
? Response.json({ tiles: [`${mirror}/planet/v/{z}/{x}/{y}.pbf`] })
: new Response(fixture('ofm-austin-14-3743-6745.pbf'));
},
tileJsonUrl: `${mirror}/planet`,
});
await source.fetchBounds(
{ south: 30.264, north: 30.271, west: -97.75, east: -97.74 },
{ zoom: 14 },
);
assert.ok(urls.length > 1, 'tiles were fetched');
assert.deepEqual(
urls.filter((url) => !url.startsWith(mirror)),
[],
'a repointed source must not fetch from tiles.openfreemap.org',
);
});
test('a mirror that serves the default host TileJSON verbatim fails loudly', async () => {
// A transparent caching proxy returns upstream's TileJSON unchanged, whose
// tiles[0] is an absolute upstream URL. That must be an error, not 100% of
// tile traffic quietly leaving the configured mirror.
const source = createOpenFreeMapSource({
fetchImpl: async () =>
Response.json({
tiles: ['https://tiles.openfreemap.org/planet/v/{z}/{x}/{y}.pbf'],
}),
tileJsonUrl: 'http://127.0.0.1:12348/planet',
});
await assert.rejects(source.getMetadata(), { retryable: false });
});
+17 -1
View File
@@ -16,11 +16,27 @@ export function validTileBounds(box) {
);
}
/**
* The origin a configured TileJSON is served from, or undefined when there is
* no absolute URL to read one off. Deriving `allowedOrigin` from it means
* repointing `tileJsonUrl` at a mirror cannot silently keep loading tiles from
* the old host: the origin check below then names the mismatch instead of
* passing it (#933).
*/
function tileJsonOrigin(tileJsonUrl) {
if (typeof tileJsonUrl !== 'string') return undefined;
try {
return new URL(tileJsonUrl).origin;
} catch {
return undefined;
}
}
/** A decoded XYZ cache with bounded workers, reads, retained bytes and request lifetimes. */
export function createVectorTileSource({
tileJsonUrl,
template,
allowedOrigin,
allowedOrigin = tileJsonOrigin(tileJsonUrl),
decode,
fetchImpl = (...args) => globalThis.fetch(...args),
maxTiles = 16,
+42
View File
@@ -203,3 +203,45 @@ test('polygon-selected tiles keep nearest-first order and cached revisits issue
'over-budget selection is rejected before fetching',
);
});
test('a configured tileJsonUrl sets the tile origin, and a foreign tiles[0] is still refused', async () => {
// Without an explicit allowedOrigin the mirror's own TileJSON is honoured…
const mirrored = createVectorTileSource({
tileJsonUrl: 'http://127.0.0.1:12348/planet',
decode: () => [],
fetchImpl: async () =>
Response.json({ tiles: ['http://127.0.0.1:12348/p/{z}/{x}/{y}.pbf'] }),
});
assert.equal(
(await mirrored.getMetadata()).template,
'http://127.0.0.1:12348/p/{z}/{x}/{y}.pbf',
);
// …while a TileJSON that names a different host is refused rather than
// silently fetched, which is what the origin check is for.
const foreign = createVectorTileSource({
tileJsonUrl: 'http://127.0.0.1:12348/planet',
decode: () => [],
fetchImpl: async () =>
Response.json({
tiles: ['https://tiles.example/planet/{z}/{x}/{y}.pbf'],
}),
});
await assert.rejects(foreign.getMetadata(), { retryable: false });
});
test('an explicit allowedOrigin still wins over the one derived from tileJsonUrl', async () => {
const source = createVectorTileSource({
tileJsonUrl: 'http://127.0.0.1:12348/planet',
allowedOrigin: 'https://tiles.example',
decode: () => [],
fetchImpl: async () =>
Response.json({
tiles: ['https://tiles.example/planet/{z}/{x}/{y}.pbf'],
}),
});
assert.equal(
(await source.getMetadata()).template,
'https://tiles.example/planet/{z}/{x}/{y}.pbf',
);
});