import express, { type Request, type Response } from "express"; import { homepageLogger } from "../../utils/logger.js"; import { safeOutboundFetch } from "../../utils/safe-outbound-fetch.js"; export const homepagePingRouter = express.Router(); interface PingCacheEntry { ok: boolean; statusCode: number | null; latencyMs: number; expires: number; } const pingCache = new Map(); const CACHE_SIZE = 200; const FETCH_TIMEOUT_MS = 5000; async function requestStatus( url: string, method: "HEAD" | "GET", ): Promise { const res = await safeOutboundFetch(url, { method, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), }); // Discard the body without buffering it. await res.body?.cancel().catch(() => {}); return res.status ?? null; } async function pingUrl( url: string, ): Promise<{ ok: boolean; statusCode: number | null; latencyMs: number }> { const start = performance.now(); const elapsed = () => Math.round(performance.now() - start); try { let code = await requestStatus(url, "HEAD"); if (code === 405) { code = await requestStatus(url, "GET"); } return { ok: code !== null && code < 400, statusCode: code, latencyMs: elapsed(), }; } catch { return { ok: false, statusCode: null, latencyMs: elapsed() }; } } /** * @openapi * /homepage/ping: * get: * summary: Check the HTTP reachability and latency of a URL * tags: * - Homepage * parameters: * - in: query * name: url * required: true * schema: * type: string * - in: query * name: ttl * schema: * type: integer * description: Cache TTL in seconds (min 10) * responses: * 200: * description: Ping result with ok, statusCode and latencyMs. * 400: * description: Invalid or missing URL. */ homepagePingRouter.get("/", async (req: Request, res: Response) => { let targetUrl = req.query.url as string; const ttl = Math.max(10, Number(req.query.ttl) || 30) * 1000; if (!targetUrl) return res.status(400).json({ error: "url is required" }); if (!/^https?:\/\//i.test(targetUrl)) targetUrl = `https://${targetUrl}`; try { new URL(targetUrl); } catch { return res.status(400).json({ error: "Invalid URL" }); } const cached = pingCache.get(targetUrl); if (cached && cached.expires > Date.now()) { return res.json({ ok: cached.ok, statusCode: cached.statusCode, latencyMs: cached.latencyMs, }); } try { const result = await pingUrl(targetUrl); if (pingCache.size >= CACHE_SIZE) { const oldest = pingCache.keys().next().value; if (oldest) pingCache.delete(oldest); } pingCache.set(targetUrl, { ...result, expires: Date.now() + ttl }); res.json(result); } catch (err) { homepageLogger.warn("Ping failed", { targetUrl }); res.status(500).json({ error: "Ping failed" }); } });