fix: Decode HTML entities in chat responses and improve n8n error handling
Some checks failed
Dev Deployment (Zero Downtime) / deploy-dev (push) Has been cancelled

- Add HTML entity decoding for chat responses (fixes ' display issue)
- Add timeout handling for n8n webhook requests (30s chat, 10s status)
- Improve error logging with detailed error information
- Add N8N_SECRET_TOKEN support for authentication
- Better fallback handling when n8n is unavailable
- Fix server-side HTML entity decoding for chat and status endpoints
This commit is contained in:
2026-01-09 14:52:06 +01:00
parent 271703556d
commit 4184e2fcf0
4 changed files with 173 additions and 54 deletions

View File

@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { decodeHtmlEntitiesServer } from "@/lib/html-decode";
export async function POST(request: NextRequest) {
let userMessage = "";
@@ -36,29 +37,43 @@ export async function POST(request: NextRequest) {
});
}
console.log(`Sending to n8n: ${n8nWebhookUrl}/webhook/chat`);
const webhookUrl = `${n8nWebhookUrl}/webhook/chat`;
console.log(`Sending to n8n: ${webhookUrl}`);
const response = await fetch(`${n8nWebhookUrl}/webhook/chat`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(process.env.N8N_API_KEY && {
Authorization: `Bearer ${process.env.N8N_API_KEY}`,
// Add timeout to prevent hanging requests
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
try {
const response = await fetch(webhookUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(process.env.N8N_SECRET_TOKEN && {
Authorization: `Bearer ${process.env.N8N_SECRET_TOKEN}`,
}),
...(process.env.N8N_API_KEY && {
"X-API-Key": process.env.N8N_API_KEY,
}),
},
body: JSON.stringify({
message: userMessage,
history: history,
}),
},
body: JSON.stringify({
message: userMessage,
history: history,
}),
});
if (!response.ok) {
console.error(`n8n webhook failed with status: ${response.status}`);
throw new Error(`n8n webhook failed: ${response.status}`);
}
signal: controller.signal,
});
const data = await response.json();
clearTimeout(timeoutId);
console.log("n8n response data:", data);
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
console.error(`n8n webhook failed with status: ${response.status}`, errorText);
throw new Error(`n8n webhook failed: ${response.status} - ${errorText}`);
}
const data = await response.json();
console.log("n8n response data:", data);
const reply =
data.reply ||
@@ -77,17 +92,41 @@ export async function POST(request: NextRequest) {
if (data && typeof data === "object" && Object.keys(data).length > 0) {
// It returned something, but we don't know what field to use.
// Check for common n8n structure
if (data.output) return NextResponse.json({ reply: data.output });
if (data.data) return NextResponse.json({ reply: data.data });
if (data.output) {
const decoded = decodeHtmlEntitiesServer(String(data.output));
return NextResponse.json({ reply: decoded });
}
if (data.data) {
const decoded = decodeHtmlEntitiesServer(String(data.data));
return NextResponse.json({ reply: decoded });
}
}
throw new Error("Invalid response format from n8n");
}
return NextResponse.json({
reply: reply,
});
} catch (error) {
// Decode HTML entities in the reply
const decodedReply = decodeHtmlEntitiesServer(String(reply));
return NextResponse.json({
reply: decodedReply,
});
} catch (fetchError: unknown) {
clearTimeout(timeoutId);
if (fetchError instanceof Error && fetchError.name === 'AbortError') {
console.error("n8n webhook request timed out");
} else {
console.error("n8n webhook fetch error:", fetchError);
}
throw fetchError;
}
} catch (error: unknown) {
console.error("Chat API error:", 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',
});
// Fallback to mock responses
// Now using the variable captured at the start

View File

@@ -20,6 +20,7 @@ export async function GET(request: NextRequest) {
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" },
@@ -31,42 +32,70 @@ export async function GET(request: NextRequest) {
// Rufe den n8n Webhook auf
// Add timestamp to query to bypass Cloudflare cache
const res = await fetch(
`${n8nWebhookUrl}/webhook/denshooter-71242/status?t=${Date.now()}`,
{
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: {
"Content-Type": "application/json",
...(process.env.N8N_SECRET_TOKEN && {
Authorization: `Bearer ${process.env.N8N_SECRET_TOKEN}`,
}),
},
next: { revalidate: 30 },
},
);
signal: controller.signal,
});
if (!res.ok) {
throw new Error(`n8n error: ${res.status}`);
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 data = await res.json();
// 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;
}
const data = await res.json();
// 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 (error) {
} 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" },

View File

@@ -125,9 +125,19 @@ export default function ChatWidget() {
const data = await response.json();
// Decode HTML entities in the reply
let replyText = data.reply || "Sorry, I couldn't process that. Please try again.";
// Decode HTML entities client-side as well (double safety)
if (typeof window !== 'undefined') {
const textarea = document.createElement('textarea');
textarea.innerHTML = replyText;
replyText = textarea.value;
}
const botMessage: Message = {
id: (Date.now() + 1).toString(),
text: data.reply || "Sorry, I couldn't process that. Please try again.",
text: replyText,
sender: "bot",
timestamp: new Date(),
};