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 <noreply@anthropic.com>
This commit is contained in:
justin 2026-05-31 10:36:28 -05:00
parent cebc432af8
commit 13492af732

View file

@ -20,15 +20,19 @@ const FMCSA_BASE = "https://mobile.fmcsa.dot.gov/qc/services/carriers";
async function fmcsaFetch(path: string): Promise<any> {
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 }),