Fix Elysia production compatibility

This commit is contained in:
Bilal Godil 2026-07-13 12:36:30 -07:00
parent f33cb4cbc5
commit 6f3960e076
4 changed files with 48 additions and 9 deletions

View File

@ -5,6 +5,7 @@ export const config = {
maxDuration: 60,
};
export default function handler(request: Request): Response | Promise<Response> {
return app.handle(request);
}
// Vercel treats a default-exported function as the legacy Node.js `(request,
// response)` handler contract. Export the Elysia app itself so Vercel detects
// its Web-standard `fetch` handler and forwards the returned Response.
export default app;

View File

@ -4,6 +4,7 @@ import { NextNotFoundError } from "@/lib/runtime/navigation";
import { parseCookieHeader, requestContextALS, type RequestContext } from "@/lib/runtime/request-context";
import { node } from "@elysiajs/node";
import { getEnvVariable, getNodeEnvironment } from "@hexclave/shared/dist/utils/env";
import { captureError } from "@hexclave/shared/dist/utils/errors";
import { Elysia } from "elysia";
import { runRequestPipeline } from "./middleware";
import { createBackendRequest } from "./backend-request";
@ -39,6 +40,13 @@ export const app = new Elysia({
const pathname = new URL(request.url).pathname;
console.log(`[Elysia] ${request.method} ${pathname} ${set.status} ${elapsedMilliseconds}ms`);
})
.onError(({ error }) => {
// Smart route handlers sanitize their own errors. This is the final boundary
// for errors from raw routes and framework code, which Elysia would otherwise
// return to the client verbatim.
captureError("elysia-request-handler", error);
return internalServerErrorResponse();
})
.get("/", () => htmlResponse(homeHtml()))
.get("/dev-stats", () => htmlResponse(devStatsHtml()))
.get("/health/error-handler-debug", () => htmlResponse(errorHandlerDebugHtml()))
@ -132,9 +140,6 @@ export async function dispatch(request: Request) {
finalResponse.headers.set(key, value);
}
}
if (pipeline.middlewareRewrite != null) {
finalResponse.headers.set("x-middleware-rewrite", pipeline.middlewareRewrite);
}
return withGlobalHeaders(finalResponse);
}
@ -164,6 +169,38 @@ function htmlResponse(body: string, status = 200) {
}));
}
function internalServerErrorResponse() {
return withGlobalHeaders(new Response("Internal Server Error", {
status: 500,
headers: {
"content-type": "text/plain; charset=utf-8",
},
}));
}
import.meta.vitest?.test("unhandled errors do not expose their message", async ({ expect }) => {
const response = internalServerErrorResponse();
expect(response.status).toBe(500);
expect(await response.text()).toBe("Internal Server Error");
expect(response.headers.get("x-content-type-options")).toBe("nosniff");
});
import.meta.vitest?.test("API version migrations do not expose their internal rewrite", async ({ expect }) => {
const { vi } = import.meta.vitest!;
vi.stubEnv("HEXCLAVE_ARTIFICIAL_DEVELOPMENT_DELAY_MS", "0");
vi.stubEnv("STACK_ARTIFICIAL_DEVELOPMENT_DELAY_MS", "0");
try {
const response = await app.handle(new Request("http://localhost/api/v2beta1/migration-tests/smart-route-handler"));
expect(response.status).toBe(200);
expect(response.headers.get("x-middleware-rewrite")).toBeNull();
} finally {
vi.unstubAllEnvs();
}
});
function homeHtml() {
const devStatsLink = getNodeEnvironment() === "development"
? `<br><a href="/dev-stats">Dev Stats</a><br>`

View File

@ -51,7 +51,6 @@ export type PipelineResult = {
corsHeadersInit?: HeadersInit,
dispatchPath: string,
mergedHeaders: Headers,
middlewareRewrite?: string,
originalUrl: string,
shortCircuitResponse?: Response,
};
@ -133,7 +132,6 @@ export async function runRequestPipeline(request: Request): Promise<PipelineResu
corsHeadersInit,
dispatchPath,
mergedHeaders,
middlewareRewrite: dispatchPath === url.pathname ? undefined : dispatchPath,
originalUrl: request.url,
};
}

View File

@ -52,7 +52,10 @@ describe("SmartRouteHandler", () => {
"headers": Headers { <some fields may have been hidden> },
}
`);
expect(response.headers.get("x-middleware-rewrite")).toBe(`/api/migrations/v2beta2/migration-tests/smart-route-handler`);
// The version migration is internal routing state. Next.js strips this
// implementation header in production, so the Elysia transport must not
// expose the rewritten path either.
expect(response.headers.get("x-middleware-rewrite")).toBeNull();
});
it("should return 200 with queryParam", async ({ expect }) => {