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
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:
@@ -1,4 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { decodeHtmlEntitiesServer } from "@/lib/html-decode";
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
let userMessage = "";
|
let userMessage = "";
|
||||||
@@ -36,24 +37,38 @@ 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`, {
|
// 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",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
...(process.env.N8N_SECRET_TOKEN && {
|
||||||
|
Authorization: `Bearer ${process.env.N8N_SECRET_TOKEN}`,
|
||||||
|
}),
|
||||||
...(process.env.N8N_API_KEY && {
|
...(process.env.N8N_API_KEY && {
|
||||||
Authorization: `Bearer ${process.env.N8N_API_KEY}`,
|
"X-API-Key": process.env.N8N_API_KEY,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
message: userMessage,
|
message: userMessage,
|
||||||
history: history,
|
history: history,
|
||||||
}),
|
}),
|
||||||
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error(`n8n webhook failed with status: ${response.status}`);
|
const errorText = await response.text().catch(() => 'Unknown error');
|
||||||
throw new Error(`n8n webhook failed: ${response.status}`);
|
console.error(`n8n webhook failed with status: ${response.status}`, errorText);
|
||||||
|
throw new Error(`n8n webhook failed: ${response.status} - ${errorText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -77,17 +92,41 @@ export async function POST(request: NextRequest) {
|
|||||||
if (data && typeof data === "object" && Object.keys(data).length > 0) {
|
if (data && typeof data === "object" && Object.keys(data).length > 0) {
|
||||||
// It returned something, but we don't know what field to use.
|
// It returned something, but we don't know what field to use.
|
||||||
// Check for common n8n structure
|
// Check for common n8n structure
|
||||||
if (data.output) return NextResponse.json({ reply: data.output });
|
if (data.output) {
|
||||||
if (data.data) return NextResponse.json({ reply: data.data });
|
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");
|
throw new Error("Invalid response format from n8n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decode HTML entities in the reply
|
||||||
|
const decodedReply = decodeHtmlEntitiesServer(String(reply));
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
reply: reply,
|
reply: decodedReply,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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("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
|
// Fallback to mock responses
|
||||||
// Now using the variable captured at the start
|
// Now using the variable captured at the start
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const n8nWebhookUrl = process.env.N8N_WEBHOOK_URL;
|
const n8nWebhookUrl = process.env.N8N_WEBHOOK_URL;
|
||||||
|
|
||||||
if (!n8nWebhookUrl) {
|
if (!n8nWebhookUrl) {
|
||||||
|
console.warn("N8N_WEBHOOK_URL not configured for status endpoint");
|
||||||
// Return fallback if n8n is not configured
|
// Return fallback if n8n is not configured
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
status: { text: "offline", color: "gray" },
|
status: { text: "offline", color: "gray" },
|
||||||
@@ -31,19 +32,32 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
// Rufe den n8n Webhook auf
|
// Rufe den n8n Webhook auf
|
||||||
// Add timestamp to query to bypass Cloudflare cache
|
// Add timestamp to query to bypass Cloudflare cache
|
||||||
const res = await fetch(
|
const statusUrl = `${n8nWebhookUrl}/webhook/denshooter-71242/status?t=${Date.now()}`;
|
||||||
`${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",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
...(process.env.N8N_SECRET_TOKEN && {
|
||||||
|
Authorization: `Bearer ${process.env.N8N_SECRET_TOKEN}`,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
next: { revalidate: 30 },
|
next: { revalidate: 30 },
|
||||||
},
|
signal: controller.signal,
|
||||||
);
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`n8n error: ${res.status}`);
|
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();
|
const data = await res.json();
|
||||||
@@ -65,8 +79,23 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json(statusData);
|
return NextResponse.json(statusData);
|
||||||
} catch (error) {
|
} 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 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
|
// Leeres Fallback-Objekt, damit die Seite nicht abstürzt
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
status: { text: "offline", color: "gray" },
|
status: { text: "offline", color: "gray" },
|
||||||
|
|||||||
@@ -125,9 +125,19 @@ export default function ChatWidget() {
|
|||||||
|
|
||||||
const data = await response.json();
|
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 = {
|
const botMessage: Message = {
|
||||||
id: (Date.now() + 1).toString(),
|
id: (Date.now() + 1).toString(),
|
||||||
text: data.reply || "Sorry, I couldn't process that. Please try again.",
|
text: replyText,
|
||||||
sender: "bot",
|
sender: "bot",
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
};
|
};
|
||||||
|
|||||||
41
lib/html-decode.ts
Normal file
41
lib/html-decode.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Decode HTML entities in strings
|
||||||
|
* Converts ' " & < > etc. to their actual characters
|
||||||
|
*/
|
||||||
|
export function decodeHtmlEntities(text: string): string {
|
||||||
|
if (!text || typeof text !== 'string') {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a temporary element to decode HTML entities
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.innerHTML = text;
|
||||||
|
return textarea.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server-side HTML entity decoding (for Node.js/Next.js API routes)
|
||||||
|
*/
|
||||||
|
export function decodeHtmlEntitiesServer(text: string): string {
|
||||||
|
if (!text || typeof text !== 'string') {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map of common HTML entities
|
||||||
|
const entityMap: Record<string, string> = {
|
||||||
|
''': "'",
|
||||||
|
'"': '"',
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
''': "'",
|
||||||
|
''': "'",
|
||||||
|
'/': '/',
|
||||||
|
'`': '`',
|
||||||
|
'=': '=',
|
||||||
|
};
|
||||||
|
|
||||||
|
return text.replace(/&[#\w]+;/g, (entity) => {
|
||||||
|
return entityMap[entity] || entity;
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user