From 44b5c5e5ca43803f1cffbb9838a919615dd7633e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 21 Apr 2026 11:30:08 +0000 Subject: [PATCH] fix: trim METABASE_SECRET_KEY and disable response caching for embed API The 'Message seems corrupt or manipulated' error from Metabase occurs when the JWT signature doesn't match the expected secret key. After a key rotation, this is commonly caused by: 1. Whitespace/newlines in the env variable value (common when copy-pasting into Vercel). Fixed by trimming the secret key before signing. 2. Stale JWTs served from CDN cache (old s-maxage=300). Fixed by switching to no-store caching so each request generates a fresh JWT with the current key. Co-authored-by: felixkrrr --- app/api/metabase-embed/route.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/app/api/metabase-embed/route.ts b/app/api/metabase-embed/route.ts index 1f4689dfba..d56c56a731 100644 --- a/app/api/metabase-embed/route.ts +++ b/app/api/metabase-embed/route.ts @@ -1,23 +1,27 @@ import { NextRequest, NextResponse } from "next/server"; import jwt from "jsonwebtoken"; +const METABASE_SITE_URL = "https://langfuse.metabaseapp.com"; + export async function GET(request: NextRequest) { try { - const METABASE_SITE_URL = "https://langfuse.metabaseapp.com"; - const METABASE_SECRET_KEY = process.env.METABASE_SECRET_KEY; + const secretKeyRaw = process.env.METABASE_SECRET_KEY; - if (!METABASE_SECRET_KEY) { - console.error("Missing required environment variables: METABASE_SECRET_KEY"); + if (!secretKeyRaw) { + console.error("Missing required environment variable: METABASE_SECRET_KEY"); return NextResponse.json( { message: "Server configuration error" }, { status: 500 } ); } + const secretKey = secretKeyRaw.trim(); + const dashboardId = request.nextUrl.searchParams.get("dashboardId") ? parseInt(request.nextUrl.searchParams.get("dashboardId")!, 10) : 25; - const theme = request.nextUrl.searchParams.get("theme") === "day" ? "day" : "night"; + const theme = + request.nextUrl.searchParams.get("theme") === "day" ? "day" : "night"; const payload = { resource: { dashboard: dashboardId }, @@ -25,7 +29,7 @@ export async function GET(request: NextRequest) { exp: Math.round(Date.now() / 1000) + 10 * 60, }; - const token = jwt.sign(payload, METABASE_SECRET_KEY); + const token = jwt.sign(payload, secretKey); const iframeUrl = `${METABASE_SITE_URL}/embed/dashboard/${token}#theme=${theme}&bordered=false&titled=false`; return NextResponse.json( @@ -35,7 +39,7 @@ export async function GET(request: NextRequest) { }, { headers: { - "Cache-Control": "public, s-maxage=300, max-age=0", + "Cache-Control": "no-store, no-cache, must-revalidate", }, } );