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:
Claude VM
2026-05-24 12:38:06 +03:00
parent 44ba50f226
commit 9c496419fd
+23 -9
View File
@@ -24,7 +24,7 @@ export const dynamic = "force-dynamic";
const ALLOWED_STYLES = new Set(["liberty", "dark", "positron", "bright"]);
const OFM_BASE = "https://tiles.openfreemap.org";
const TILE_PROXY_PREFIX = "/api/basemap-tile";
const TILE_PROXY_PATH = "/api/basemap-tile";
type StyleSource = {
type: string;
@@ -62,20 +62,31 @@ async function fetchJson<T>(url: string): Promise<T> {
/** Replace `https://tiles.openfreemap.org/<rest>` with the architots
* proxy prefix so the browser never talks to openfreemap directly.
* Leaves `{z}/{x}/{y}` template tokens intact. */
function rewriteUpstreamUrl(value: string): string {
* Emits ABSOLUTE URLs — MapLibre loads tiles in a Web Worker context
* 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 (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;
}
export async function GET(
_request: Request,
request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
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)) {
return NextResponse.json({ error: "unknown_style" }, { status: 404 });
}
@@ -119,7 +130,7 @@ export async function GET(
// that point.
for (const source of Object.values(resolved)) {
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.
let sprite: unknown = style.sprite;
if (typeof sprite === "string") {
sprite = rewriteUpstreamUrl(sprite);
sprite = rewriteUpstreamUrl(sprite, origin);
} else if (Array.isArray(sprite)) {
sprite = sprite.map((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,
);
}
const glyphs =
typeof style.glyphs === "string"
? rewriteUpstreamUrl(style.glyphs)
? rewriteUpstreamUrl(style.glyphs, origin)
: style.glyphs;
return NextResponse.json(