From 13492af732a556c863ce87ffce1e132a2df223be Mon Sep 17 00:00:00 2001 From: justin Date: Sun, 31 May 2026 10:36:28 -0500 Subject: [PATCH] dot-lookup: fix hanging FMCSA fetch with AbortController (not AbortSignal.timeout) AbortSignal.timeout() requires Node 17.3+. The API container likely runs an older Node version, so timeouts never fired -> fetch hung forever when FMCSA API is down -> nginx proxy timeout -> 'Failed to fetch' in the browser. Fix: use AbortController + manual setTimeout() which works on all Node versions. All 3 external fetch points (fmcsaFetch x2, SOS x2) now actually abort at 5s. Also: guard final res.json() with !res.headersSent so the 12s deadline fallback and the normal response path can't double-send. Co-Authored-By: Claude Sonnet 4.6 --- api/src/routes/dot-lookup.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/api/src/routes/dot-lookup.ts b/api/src/routes/dot-lookup.ts index a55dda1..7078ad4 100644 --- a/api/src/routes/dot-lookup.ts +++ b/api/src/routes/dot-lookup.ts @@ -20,15 +20,19 @@ const FMCSA_BASE = "https://mobile.fmcsa.dot.gov/qc/services/carriers"; async function fmcsaFetch(path: string): Promise { if (!FMCSA_API_KEY) return null; + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 5000); try { const url = `${FMCSA_BASE}/${path}${path.includes("?") ? "&" : "?"}webKey=${FMCSA_API_KEY}`; const resp = await fetch(url, { - signal: AbortSignal.timeout(5000), + signal: controller.signal, headers: { "Accept": "application/json" }, }); + clearTimeout(timer); if (!resp.ok) return null; return await resp.json(); } catch { + clearTimeout(timer); return null; } } @@ -515,7 +519,7 @@ router.get("/api/v1/dot/lookup", async (req, res) => { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ entity_name: name, state_code: state }), - signal: AbortSignal.timeout(5000), + signal: (() => { const c = new AbortController(); setTimeout(() => c.abort(), 5000); return c.signal; })(), }); if (sosResp.ok) { sosStatus = (await sosResp.json()) as { found?: boolean; status?: string; error?: string }; @@ -593,6 +597,7 @@ router.get("/api/v1/dot/lookup", async (req, res) => { const redCount = checks.filter(c => c.status === "red").length; const yellowCount = checks.filter(c => c.status === "yellow").length; + if (res.headersSent) return; // deadline already responded res.json({ dot_number: rawDot, legal_name: name, @@ -722,7 +727,7 @@ router.post("/api/v1/dot/name-check", async (req, res) => { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ entity_name: name, state_code: stateCode }), - signal: AbortSignal.timeout(5000), + signal: (() => { const c = new AbortController(); setTimeout(() => c.abort(), 5000); return c.signal; })(), }).then(r => r.json()) : Promise.resolve({ skipped: true }),