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).
147 lines
4.5 KiB
TypeScript
147 lines
4.5 KiB
TypeScript
/**
|
|
* 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<string, string> = {
|
|
"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;
|
|
},
|
|
});
|