server sided external api calls (#697)

* server sided external api calls

* fix uploading gpx files

* rename api/v1/nominatim with api/v1/geocoding to prepare photon support

* remove NOMINATIM_MAX_RETRIES (side-kick from other PR)

* re-add nominatim url fallback

* docu, env var defaults and fallbacks, docker compose files

* refactor

* furhter refactorings and fixes

* PUBLIC_VALHALLA_ENABLED in docker compose files added

* fix env var names

---------

Co-authored-by: Flomp <Flomp@users.noreply.github.com>
This commit is contained in:
slothful-vassal
2026-04-28 18:19:06 +02:00
committed by GitHub
parent 7bdf0d3151
commit 85e5fb6df7
21 changed files with 391 additions and 100 deletions

View File

@@ -0,0 +1,18 @@
import { json } from "@sveltejs/kit";
function safeJson(text: string): any {
try {
return JSON.parse(text);
} catch {
return { message: text };
}
}
export async function proxyJsonResponse(response: Response) {
const text = await response.text();
const payload = text.length ? safeJson(text) : {};
if (!response.ok) {
return json(payload, { status: response.status });
}
return json(payload);
}

View File

@@ -0,0 +1,63 @@
import { version } from "$app/environment";
import { resolveBaseUrl } from "$lib/server/url";
import type { RequestEvent } from "@sveltejs/kit";
const NOMINATIM_RATE_LIMIT_MS = 1000;
const NOMINATIM_MAX_RETRIES = 2;
let lastNominatimCall = 0;
function getNominatimBaseUrl(): string {
return resolveBaseUrl("NOMINATIM_URL", "https://nominatim.openstreetmap.org");
}
function needsRateLimiting(baseUrl: string): boolean {
return baseUrl.includes("nominatim.openstreetmap.org");
}
const waitTimer = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
async function nominatimRateLimiter(baseUrl: string) {
if (!needsRateLimiting(baseUrl)) {
return;
}
const elapsedTimeMs = Date.now() - lastNominatimCall;
const waitTime = NOMINATIM_RATE_LIMIT_MS - elapsedTimeMs;
if (waitTime > 0) {
await waitTimer(waitTime);
}
lastNominatimCall = Date.now();
}
export async function fetchNominatim(event: RequestEvent, path: string, params: URLSearchParams): Promise<Response> {
const baseUrl = getNominatimBaseUrl();
const base = new URL(baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
const cleanPath = path.replace(/^\/+/, "");
const url = new URL(cleanPath, base);
const query = params.toString();
if (query.length) {
url.search = query;
}
let attempt = 0;
while (true) {
await nominatimRateLimiter(baseUrl);
try {
return await event.fetch(url.toString(), {
method: "GET",
headers: {
"User-Agent": `wanderer/${version}`,
},
});
} catch (error) {
if (attempt < NOMINATIM_MAX_RETRIES) {
attempt++;
continue;
}
throw new Error(`Nominatim fetch failed for ${url.toString()}`, { cause: error });
}
}
}

View File

@@ -0,0 +1,34 @@
import { resolveBaseUrl } from "$lib/server/url";
import type { RequestEvent } from "@sveltejs/kit";
const OVERPASS_MAX_RETRIES = 2;
function getOverpassBaseUrl(): string {
return resolveBaseUrl("OVERPASS_API_URL", "https://overpass-api.de");
}
export async function fetchOverpass(event: RequestEvent, params: URLSearchParams): Promise<Response> {
const baseUrl = getOverpassBaseUrl();
const base = new URL(baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
const url = new URL("api/interpreter", base);
const query = params.toString();
if (query.length) {
url.search = query;
}
let attempt = 0;
while (true) {
try {
return await event.fetch(url.toString(), {
method: "GET",
});
} catch (error) {
if (attempt < OVERPASS_MAX_RETRIES) {
attempt++;
continue;
}
throw error;
}
}
}

24
web/src/lib/server/url.ts Normal file
View File

@@ -0,0 +1,24 @@
import { env as privateEnv } from "$env/dynamic/private";
import { env as publicEnv } from "$env/dynamic/public";
export type ExternalServiceUrlKey = "VALHALLA_URL" | "NOMINATIM_URL" | "OVERPASS_API_URL";
export function normalizeBaseUrl(url: string): string {
const trimmedUrl = url.trim();
if (!trimmedUrl) {
return "";
}
if (!/^https?:\/\//i.test(trimmedUrl)) {
return `https://${trimmedUrl}`;
}
return trimmedUrl;
}
export function resolveBaseUrl(
key: ExternalServiceUrlKey,
fallback: string = "",
): string {
const publicKey = `PUBLIC_${key}` as `PUBLIC_${string}`;
const rawUrl = privateEnv[key] ?? publicEnv[publicKey] ?? fallback;
return normalizeBaseUrl(rawUrl);
}

View File

@@ -0,0 +1,5 @@
import { resolveBaseUrl } from "$lib/server/url";
export function getValhallaBaseUrl(): string {
return resolveBaseUrl("VALHALLA_URL");
}