Files
portfolio/app/api/n8n/status/route.ts
Cursor Agent fbce838d3f fix(consent): avoid banner flashing on reload
Initialize consent state from cookie synchronously so the banner only shows when no choice was made.

fix(api): fail-soft when DB schema missing

Return null/empty content for CMS endpoints when migrations are not applied instead of crashing with Prisma P2021/P2022.

fix(n8n): parse status response defensively

Handle empty/invalid JSON bodies from n8n to prevent activity feed from getting stuck.

Co-authored-by: dennis <dennis@konkol.net>
2026-01-14 21:47:31 +00:00

134 lines
4.6 KiB
TypeScript

// app/api/n8n/status/route.ts
import { NextRequest, NextResponse } from "next/server";
// Cache für 30 Sekunden, damit wir n8n nicht zuspammen
export const revalidate = 30;
export async function GET(request: NextRequest) {
// Rate limiting for n8n status endpoint
const ip =
request.headers.get("x-forwarded-for") ||
request.headers.get("x-real-ip") ||
"unknown";
const ua = request.headers.get("user-agent") || "unknown";
const { checkRateLimit } = await import('@/lib/auth');
// In dev, many requests can share ip=unknown; use UA to avoid a shared bucket.
const rateKey =
process.env.NODE_ENV === "development" && ip === "unknown"
? `ua:${ua.slice(0, 120)}`
: ip;
const maxPerMinute = process.env.NODE_ENV === "development" ? 300 : 30;
if (!checkRateLimit(rateKey, maxPerMinute, 60000)) { // requests per minute
return NextResponse.json(
{ error: 'Rate limit exceeded. Please try again later.' },
{ status: 429 }
);
}
try {
// Check if n8n webhook URL is configured
const n8nWebhookUrl = process.env.N8N_WEBHOOK_URL;
if (!n8nWebhookUrl) {
console.warn("N8N_WEBHOOK_URL not configured for status endpoint");
// Return fallback if n8n is not configured
return NextResponse.json({
status: { text: "offline", color: "gray" },
music: null,
gaming: null,
coding: null,
});
}
// Rufe den n8n Webhook auf
// Add timestamp to query to bypass Cloudflare cache
const statusUrl = `${n8nWebhookUrl}/webhook/denshooter-71242/status?t=${Date.now()}`;
console.log(`Fetching status from: ${statusUrl}`);
// Add timeout to prevent hanging requests
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
try {
const res = await fetch(statusUrl, {
method: "GET",
headers: {
// n8n sometimes responds with empty body; we'll parse defensively below.
Accept: "application/json",
...(process.env.N8N_SECRET_TOKEN && {
Authorization: `Bearer ${process.env.N8N_SECRET_TOKEN}`,
}),
},
next: { revalidate: 30 },
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!res.ok) {
const errorText = await res.text().catch(() => 'Unknown error');
console.error(`n8n status webhook failed: ${res.status}`, errorText);
throw new Error(`n8n error: ${res.status} - ${errorText}`);
}
const raw = await res.text().catch(() => "");
if (!raw || !raw.trim()) {
throw new Error("Empty response body received from n8n");
}
let data: unknown;
try {
data = JSON.parse(raw);
} catch (parseError) {
// Sometimes upstream sends HTML or a partial response; include a snippet for debugging.
const snippet = raw.slice(0, 240);
throw new Error(
`Invalid JSON from n8n (${res.status}): ${snippet}${raw.length > 240 ? "…" : ""}`,
);
}
// n8n gibt oft ein Array zurück: [{...}]. Wir wollen nur das Objekt.
const statusData = Array.isArray(data) ? data[0] : data;
// Safety check: if statusData is still undefined/null (e.g. empty array), use fallback
if (!statusData) {
throw new Error("Empty data received from n8n");
}
// Ensure coding object has proper structure
if (statusData.coding && typeof statusData.coding === "object") {
// Already properly formatted from n8n
} else if (statusData.coding === null || statusData.coding === undefined) {
// No coding data - keep as null
statusData.coding = null;
}
return NextResponse.json(statusData);
} catch (fetchError: unknown) {
clearTimeout(timeoutId);
if (fetchError instanceof Error && fetchError.name === 'AbortError') {
console.error("n8n status webhook request timed out");
} else {
console.error("n8n status webhook fetch error:", fetchError);
}
throw fetchError;
}
} catch (error: unknown) {
console.error("Error fetching n8n status:", error);
console.error("Error details:", {
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
n8nUrl: process.env.N8N_WEBHOOK_URL ? 'configured' : 'missing',
});
// Leeres Fallback-Objekt, damit die Seite nicht abstürzt
return NextResponse.json({
status: { text: "offline", color: "gray" },
music: null,
gaming: null,
coding: null,
});
}
}