From 6666cc5f67e1079c51fc927ae7ec5bb59cf1fe39 Mon Sep 17 00:00:00 2001 From: Rimuru Date: Thu, 11 Jun 2026 15:36:44 +0530 Subject: [PATCH] fix(frontend): replace broken file-based API routes with SolidStart middleware Vinxi 0.5.7 + @solidjs/start 1.3.2 has a build bug where file-based API routes (src/routes/api/*) are registered in the page router tree but never mounted as Nitro handlers in the production build, so every /api/* request returns a framework 404. Fix: register a SolidStart middleware (src/middleware.ts) via the middleware config field. The middleware intercepts all /api/* paths and proxies them to the Rust gateway, bypassing the broken page router. Covers: - /api/gateway/* (catch-all proxy to gateway) - /api/kb/categories - /api/kb/articles - /api/kb/articles/:slug Also tightens the dev-server vite proxy from /api to /api/kb so it doesn't shadow the new middleware in dev. Removes the dead src/routes/api/ tree (no longer used). --- src/middleware.ts | 147 +++++++++++++++++++++++++++ src/routes/api/gateway/[...path].ts | 87 ---------------- src/routes/api/kb/articles.ts | 19 ---- src/routes/api/kb/articles/[slug].ts | 18 ---- src/routes/api/kb/categories.ts | 18 ---- vite.config.ts | 6 +- 6 files changed, 152 insertions(+), 143 deletions(-) create mode 100644 src/middleware.ts delete mode 100644 src/routes/api/gateway/[...path].ts delete mode 100644 src/routes/api/kb/articles.ts delete mode 100644 src/routes/api/kb/articles/[slug].ts delete mode 100644 src/routes/api/kb/categories.ts diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..00ee394 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,147 @@ +/** + * 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; + }, +}); diff --git a/src/routes/api/gateway/[...path].ts b/src/routes/api/gateway/[...path].ts deleted file mode 100644 index 42c04b9..0000000 --- a/src/routes/api/gateway/[...path].ts +++ /dev/null @@ -1,87 +0,0 @@ -import { gatewayUrl, withAuthHeaders } from '~/lib/server/gateway'; - -/** - * Generic gateway proxy endpoint - * Forwards all requests to the Rust backend gateway with proper auth headers - * Usage: /api/gateway/api/companies/jobs → forwards to gateway /api/companies/jobs - */ -export async function GET({ request, params }: { request: Request; params: any }) { - return proxyRequest('GET', request, params); -} - -export async function POST({ request, params }: { request: Request; params: any }) { - return proxyRequest('POST', request, params); -} - -export async function PUT({ request, params }: { request: Request; params: any }) { - return proxyRequest('PUT', request, params); -} - -export async function DELETE({ request, params }: { request: Request; params: any }) { - return proxyRequest('DELETE', request, params); -} - -export async function PATCH({ request, params }: { request: Request; params: any }) { - return proxyRequest('PATCH', request, params); -} - -async function proxyRequest(method: string, request: Request, params: any) { - try { - // Handle different param structures - let pathArray = params.path; - if (!Array.isArray(pathArray)) { - pathArray = [pathArray]; - } - - const rawPath = `/${pathArray.join('/')}`; - // Normalize all forwarded routes to the Rust gateway's /api/* contract. - const path = rawPath.startsWith('/api/') || rawPath === '/api' - ? rawPath - : `/api${rawPath}`; - - // Preserve query string - const url = new URL(request.url); - const queryString = url.search ? url.search : ''; - - // Build request body if needed - let body: string | undefined; - if (['POST', 'PUT', 'PATCH'].includes(method)) { - body = await request.text(); - } - - // Forward to gateway - const upstreamUrl = gatewayUrl(path + queryString); - const upstreamRequest = new Request(upstreamUrl, { - method, - headers: withAuthHeaders(request, { - 'Content-Type': request.headers.get('Content-Type') || 'application/json', - }), - body, - cache: 'no-store', - }); - - const response = await fetch(upstreamRequest); - - // Copy response headers and return - const responseHeaders = new Headers(); - response.headers.forEach((value, key) => { - if (!['server', 'transfer-encoding', 'connection'].includes(key.toLowerCase())) { - responseHeaders.set(key, value); - } - }); - responseHeaders.set('Content-Type', 'application/json'); - - const responseBody = await response.text(); - - return new Response(responseBody, { - status: response.status, - statusText: response.statusText, - headers: responseHeaders, - }); - } catch (error: any) { - return new Response( - JSON.stringify({ success: false, error: error?.message || 'Internal Server Error' }), - { status: 500, headers: { 'Content-Type': 'application/json' } }, - ); - } -} diff --git a/src/routes/api/kb/articles.ts b/src/routes/api/kb/articles.ts deleted file mode 100644 index b35e937..0000000 --- a/src/routes/api/kb/articles.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { gatewayUrl } from '~/lib/server/gateway'; - -export async function GET({ request }: { request: Request }) { - const url = new URL(request.url); - const upstream = gatewayUrl('/api/kb/articles' + url.search); - try { - const res = await fetch(upstream, { cache: 'no-store' }); - const body = await res.text(); - return new Response(body, { - status: res.status, - headers: { 'Content-Type': 'application/json' }, - }); - } catch (err: any) { - return new Response(JSON.stringify({ error: err?.message || 'Gateway error' }), { - status: 502, - headers: { 'Content-Type': 'application/json' }, - }); - } -} diff --git a/src/routes/api/kb/articles/[slug].ts b/src/routes/api/kb/articles/[slug].ts deleted file mode 100644 index 41e416f..0000000 --- a/src/routes/api/kb/articles/[slug].ts +++ /dev/null @@ -1,18 +0,0 @@ -import { gatewayUrl } from '~/lib/server/gateway'; - -export async function GET({ params }: { params: { slug: string } }) { - const upstream = gatewayUrl(`/api/kb/articles/${params.slug}`); - try { - const res = await fetch(upstream, { cache: 'no-store' }); - const body = await res.text(); - return new Response(body, { - status: res.status, - headers: { 'Content-Type': 'application/json' }, - }); - } catch (err: any) { - return new Response(JSON.stringify({ error: err?.message || 'Gateway error' }), { - status: 502, - headers: { 'Content-Type': 'application/json' }, - }); - } -} diff --git a/src/routes/api/kb/categories.ts b/src/routes/api/kb/categories.ts deleted file mode 100644 index 9b64a4f..0000000 --- a/src/routes/api/kb/categories.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { gatewayUrl } from '~/lib/server/gateway'; - -export async function GET() { - const upstream = gatewayUrl('/api/kb/categories'); - try { - const res = await fetch(upstream, { cache: 'no-store' }); - const body = await res.text(); - return new Response(body, { - status: res.status, - headers: { 'Content-Type': 'application/json' }, - }); - } catch (err: any) { - return new Response(JSON.stringify({ error: err?.message || 'Gateway error' }), { - status: 502, - headers: { 'Content-Type': 'application/json' }, - }); - } -} diff --git a/vite.config.ts b/vite.config.ts index 605211c..b38229b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,6 +2,10 @@ import { defineConfig } from "@solidjs/start/config"; import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ + // Register our Nitro middleware that handles all /api/* paths. + // Workaround for Vinxi 0.5.7 + @solidjs/start 1.3.2 build issue + // where file-based API routes are not mounted as Nitro handlers. + middleware: "./src/middleware.ts", vite: { plugins: [tailwindcss()], server: { @@ -14,7 +18,7 @@ export default defineConfig({ .replace(/^\/api\/gateway\/api(\/|$)/, "/api$1") .replace(/^\/api\/gateway(\/|$)/, "/api$1"), }, - "/api": { + "/api/kb": { target: "http://localhost:9100", changeOrigin: true, },