/** * SolidStart server middleware (runs on every request). * * Workaround for a Vinxi 0.5.7 + @solidjs/start 1.3.2 build issue where * file-based API routes in `src/routes/api/*` are registered in the page * router tree but never mounted as Nitro handlers, so every `/api/*` * request returns a 404 from the SolidStart page renderer. * * This middleware intercepts `/api/*` paths at the SolidStart middleware * layer (which IS in the request pipeline) and proxies them to the Rust * gateway. * * Responsibilities: * - /api/gateway/*path → proxy to Rust gateway * - /api/kb/categories → proxy to Rust gateway * - /api/kb/articles → proxy to Rust gateway * - /api/kb/articles/:slug → proxy to Rust gateway * * Uses the @solidjs/start `createMiddleware` pattern, which Vinxi/h3 will * actually invoke via the `onRequest` hook. */ import { createMiddleware } from "@solidjs/start/middleware"; const GATEWAY_URL = ( process.env.GATEWAY_URL || "http://nxtgauge-rust-gateway:9100" ).replace(/\/+$/, ""); const PUBLIC_API_URL = ( process.env.PUBLIC_API_URL || process.env.NEXT_PUBLIC_API_URL || `${GATEWAY_URL}/api` ).replace(/\/+$/, ""); function buildUpstream(path: string, query: string = ""): string { // PUBLIC_API_URL ends with /api; path starts with /api/... // Strip the /api prefix from path so we don't double up. if (PUBLIC_API_URL.endsWith("/api")) { const stripped = path.replace(/^\/api/, ""); return `${PUBLIC_API_URL}${stripped}${query}`; } return `${PUBLIC_API_URL}${path}${query}`; } async function proxyToGateway(fetchEvent: any, upstreamPath: string) { const req = fetchEvent.request; const method = req.method.toUpperCase(); const url = new URL(req.url); const queryString = url.search || ""; // Read body for methods that have one let body: BodyInit | undefined; if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) { try { body = await req.clone().text(); } catch { body = undefined; } } // Forward auth + content-type + cookie const headers: Record = { "Content-Type": req.headers.get("content-type") || "application/json", }; const auth = req.headers.get("authorization"); if (auth) headers["Authorization"] = auth; const cookie = req.headers.get("cookie"); if (cookie) headers["Cookie"] = cookie; const upstream = buildUpstream(upstreamPath, queryString); let response: Response; try { response = await fetch(upstream, { method, headers, body, cache: "no-store", }); } catch (err: any) { return new Response( JSON.stringify({ success: false, error: `Gateway unreachable: ${err?.message || "unknown"}`, }), { status: 502, headers: { "Content-Type": "application/json" }, }, ); } // Copy response headers (skip hop-by-hop), ensure Content-Type const respHeaders = new Headers(); response.headers.forEach((value, key) => { const k = key.toLowerCase(); if (k === "server" || k === "transfer-encoding" || k === "connection") return; respHeaders.set(key, value); }); if (!respHeaders.get("content-type")) { respHeaders.set("Content-Type", "application/json"); } const respBody = await response.text(); return new Response(respBody, { status: response.status, statusText: response.statusText, headers: respHeaders, }); } export default createMiddleware({ onRequest: async (fetchEvent) => { const url = new URL(fetchEvent.request.url); const path = url.pathname; // Only handle /api/* paths if (!path.startsWith("/api/")) return; // Gateway proxy catch-all: /api/gateway/* → strip /api/gateway, send rest if (path === "/api/gateway" || path.startsWith("/api/gateway/")) { const subPath = path.slice("/api/gateway".length) || "/"; // Normalize to /api/... contract for the Rust gateway const normalized = subPath.startsWith("/api/") || subPath === "/api" ? subPath : `/api${subPath}`; return proxyToGateway(fetchEvent, normalized); } // Knowledge base routes if (path === "/api/kb/categories") { return proxyToGateway(fetchEvent, "/api/kb/categories"); } if (path === "/api/kb/articles") { return proxyToGateway(fetchEvent, "/api/kb/articles"); } if (path.startsWith("/api/kb/articles/")) { return proxyToGateway(fetchEvent, path); } // Everything else under /api/* — let it fall through. // Returning undefined tells the framework to continue. return; }, });