Merge pull request #437 from trails-cool/fix/planner-brouter-timeout
fix(planner/brouter): add 30s timeout to outbound fetch
This commit is contained in:
commit
d07207464e
3 changed files with 46 additions and 2 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
import { mergeGeoJsonSegments, type EnrichedRoute, type NoGoArea, type Waypoint } from "./route-merge";
|
import { mergeGeoJsonSegments, type EnrichedRoute, type NoGoArea, type Waypoint } from "./route-merge";
|
||||||
|
import { fetchWithTimeout } from "./http.server";
|
||||||
|
|
||||||
export { mergeGeoJsonSegments };
|
export { mergeGeoJsonSegments };
|
||||||
export type { EnrichedRoute, NoGoArea };
|
export type { EnrichedRoute, NoGoArea };
|
||||||
|
|
@ -63,7 +64,7 @@ export class BRouterError extends Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchSegment(url: string): Promise<Record<string, unknown>> {
|
async function fetchSegment(url: string): Promise<Record<string, unknown>> {
|
||||||
const response = await fetch(url, { headers: authHeaders() });
|
const response = await fetchWithTimeout(url, { headers: authHeaders() });
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const body = await response.text();
|
const body = await response.text();
|
||||||
throw new BRouterError(body.trim(), response.status);
|
throw new BRouterError(body.trim(), response.status);
|
||||||
|
|
@ -144,7 +145,7 @@ export async function computeSegmentGpx(request: {
|
||||||
const nogoParam = request.noGoAreas?.length ? noGoAreasToParam(request.noGoAreas) : undefined;
|
const nogoParam = request.noGoAreas?.length ? noGoAreasToParam(request.noGoAreas) : undefined;
|
||||||
if (nogoParam) params.set("polygons", nogoParam);
|
if (nogoParam) params.set("polygons", nogoParam);
|
||||||
|
|
||||||
const resp = await fetch(`${BROUTER_URL}/brouter?${params}`, {
|
const resp = await fetchWithTimeout(`${BROUTER_URL}/brouter?${params}`, {
|
||||||
headers: authHeaders(),
|
headers: authHeaders(),
|
||||||
});
|
});
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
|
|
|
||||||
28
apps/planner/app/lib/http.server.test.ts
Normal file
28
apps/planner/app/lib/http.server.test.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||||
|
import { fetchWithTimeout } from "./http.server.ts";
|
||||||
|
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetchWithTimeout (planner)", () => {
|
||||||
|
it("aborts when the upstream exceeds the timeout", async () => {
|
||||||
|
globalThis.fetch = vi.fn((_input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
|
return new Promise((_resolve, reject) => {
|
||||||
|
init?.signal?.addEventListener("abort", () => {
|
||||||
|
reject(new DOMException("aborted", "AbortError"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}) as typeof fetch;
|
||||||
|
await expect(fetchWithTimeout("https://example.test", {}, 25)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the response when the call resolves in time", async () => {
|
||||||
|
globalThis.fetch = vi.fn(async () => new Response("ok", { status: 200 })) as typeof fetch;
|
||||||
|
const resp = await fetchWithTimeout("https://example.test", {}, 1000);
|
||||||
|
expect(resp.status).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
15
apps/planner/app/lib/http.server.ts
Normal file
15
apps/planner/app/lib/http.server.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
// Default timeout for outbound HTTP calls to third-party services
|
||||||
|
// (BRouter, Overpass, …). A hung upstream must not stall the request
|
||||||
|
// handler indefinitely.
|
||||||
|
export const DEFAULT_EXTERNAL_FETCH_TIMEOUT_MS = 30_000;
|
||||||
|
|
||||||
|
export function fetchWithTimeout(
|
||||||
|
input: RequestInfo | URL,
|
||||||
|
init: RequestInit = {},
|
||||||
|
timeoutMs: number = DEFAULT_EXTERNAL_FETCH_TIMEOUT_MS,
|
||||||
|
): Promise<Response> {
|
||||||
|
const signal = init.signal
|
||||||
|
? AbortSignal.any([init.signal, AbortSignal.timeout(timeoutMs)])
|
||||||
|
: AbortSignal.timeout(timeoutMs);
|
||||||
|
return fetch(input, { ...init, signal });
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue