// 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, }); } }