From 6f3960e0766c789e33e251959fbece2941b9f8d7 Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Mon, 13 Jul 2026 12:36:30 -0700 Subject: [PATCH] Fix Elysia production compatibility --- apps/backend/api/index.ts | 7 +-- apps/backend/src/server/app.ts | 43 +++++++++++++++++-- apps/backend/src/server/middleware.ts | 2 - .../endpoints/api/migration-tests.test.ts | 5 ++- 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/apps/backend/api/index.ts b/apps/backend/api/index.ts index 69025b3ef..4c53c3c07 100644 --- a/apps/backend/api/index.ts +++ b/apps/backend/api/index.ts @@ -5,6 +5,7 @@ export const config = { maxDuration: 60, }; -export default function handler(request: Request): Response | Promise { - 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; diff --git a/apps/backend/src/server/app.ts b/apps/backend/src/server/app.ts index 6c8be7203..40631e44c 100644 --- a/apps/backend/src/server/app.ts +++ b/apps/backend/src/server/app.ts @@ -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" ? `
Dev Stats
` diff --git a/apps/backend/src/server/middleware.ts b/apps/backend/src/server/middleware.ts index a769f05c2..93ec12903 100644 --- a/apps/backend/src/server/middleware.ts +++ b/apps/backend/src/server/middleware.ts @@ -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 { "headers": Headers {