fix(basemap-style): emit absolute URLs so MapLibre worker can fetch
The previous rewrite produced relative `/api/basemap-tile/…` URLs in the resolved style. MapLibre loads vector tiles + sprites + glyphs inside a Web Worker, where relative URLs have no base context and fetch() rejects them with "Failed to construct 'Request': Failed to parse URL". Browser console filled with one such error per tile request → empty cream map all over again. Fix: prepend the request origin (honoring Traefik's x-forwarded-proto / x-forwarded-host) so every rewritten URL is absolute. Same behaviour from the main thread; Web Worker fetch also works because it now has a parseable URL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@ export const dynamic = "force-dynamic";
|
|||||||
|
|
||||||
const ALLOWED_STYLES = new Set(["liberty", "dark", "positron", "bright"]);
|
const ALLOWED_STYLES = new Set(["liberty", "dark", "positron", "bright"]);
|
||||||
const OFM_BASE = "https://tiles.openfreemap.org";
|
const OFM_BASE = "https://tiles.openfreemap.org";
|
||||||
const TILE_PROXY_PREFIX = "/api/basemap-tile";
|
const TILE_PROXY_PATH = "/api/basemap-tile";
|
||||||
|
|
||||||
type StyleSource = {
|
type StyleSource = {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -62,20 +62,31 @@ async function fetchJson<T>(url: string): Promise<T> {
|
|||||||
|
|
||||||
/** Replace `https://tiles.openfreemap.org/<rest>` with the architots
|
/** Replace `https://tiles.openfreemap.org/<rest>` with the architots
|
||||||
* proxy prefix so the browser never talks to openfreemap directly.
|
* proxy prefix so the browser never talks to openfreemap directly.
|
||||||
* Leaves `{z}/{x}/{y}` template tokens intact. */
|
* Emits ABSOLUTE URLs — MapLibre loads tiles in a Web Worker context
|
||||||
function rewriteUpstreamUrl(value: string): string {
|
* where relative URLs fail to parse ("Failed to construct 'Request':
|
||||||
|
* Failed to parse URL"). Leaves `{z}/{x}/{y}` template tokens intact. */
|
||||||
|
function rewriteUpstreamUrl(value: string, origin: string): string {
|
||||||
if (typeof value !== "string") return value;
|
if (typeof value !== "string") return value;
|
||||||
if (value.startsWith(OFM_BASE)) {
|
if (value.startsWith(OFM_BASE)) {
|
||||||
return `${TILE_PROXY_PREFIX}${value.slice(OFM_BASE.length)}`;
|
return `${origin}${TILE_PROXY_PATH}${value.slice(OFM_BASE.length)}`;
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
_request: Request,
|
request: Request,
|
||||||
{ params }: { params: Promise<{ id: string }> },
|
{ params }: { params: Promise<{ id: string }> },
|
||||||
) {
|
) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
// Build the absolute origin used in rewritten URLs. Honor proxy
|
||||||
|
// headers (Traefik forwards x-forwarded-{proto,host}) so URLs match
|
||||||
|
// the user's external view of the site even when the Next process
|
||||||
|
// sees the request on its internal hostname:port.
|
||||||
|
const proto =
|
||||||
|
request.headers.get("x-forwarded-proto") ?? new URL(request.url).protocol.replace(":", "");
|
||||||
|
const host =
|
||||||
|
request.headers.get("x-forwarded-host") ?? request.headers.get("host") ?? new URL(request.url).host;
|
||||||
|
const origin = `${proto}://${host}`;
|
||||||
if (!ALLOWED_STYLES.has(id)) {
|
if (!ALLOWED_STYLES.has(id)) {
|
||||||
return NextResponse.json({ error: "unknown_style" }, { status: 404 });
|
return NextResponse.json({ error: "unknown_style" }, { status: 404 });
|
||||||
}
|
}
|
||||||
@@ -119,7 +130,7 @@ export async function GET(
|
|||||||
// that point.
|
// that point.
|
||||||
for (const source of Object.values(resolved)) {
|
for (const source of Object.values(resolved)) {
|
||||||
if (Array.isArray(source.tiles)) {
|
if (Array.isArray(source.tiles)) {
|
||||||
source.tiles = source.tiles.map(rewriteUpstreamUrl);
|
source.tiles = source.tiles.map((u) => rewriteUpstreamUrl(u, origin));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,18 +139,21 @@ export async function GET(
|
|||||||
// multi-image-set styles). Handle both.
|
// multi-image-set styles). Handle both.
|
||||||
let sprite: unknown = style.sprite;
|
let sprite: unknown = style.sprite;
|
||||||
if (typeof sprite === "string") {
|
if (typeof sprite === "string") {
|
||||||
sprite = rewriteUpstreamUrl(sprite);
|
sprite = rewriteUpstreamUrl(sprite, origin);
|
||||||
} else if (Array.isArray(sprite)) {
|
} else if (Array.isArray(sprite)) {
|
||||||
sprite = sprite.map((s) =>
|
sprite = sprite.map((s) =>
|
||||||
s && typeof s === "object" && "url" in s
|
s && typeof s === "object" && "url" in s
|
||||||
? { ...s, url: rewriteUpstreamUrl(String((s as { url: unknown }).url)) }
|
? {
|
||||||
|
...s,
|
||||||
|
url: rewriteUpstreamUrl(String((s as { url: unknown }).url), origin),
|
||||||
|
}
|
||||||
: s,
|
: s,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const glyphs =
|
const glyphs =
|
||||||
typeof style.glyphs === "string"
|
typeof style.glyphs === "string"
|
||||||
? rewriteUpstreamUrl(style.glyphs)
|
? rewriteUpstreamUrl(style.glyphs, origin)
|
||||||
: style.glyphs;
|
: style.glyphs;
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
Reference in New Issue
Block a user