Files
portfolio/app/api/n8n/chat/route.ts
denshooter 4184e2fcf0
Some checks failed
Dev Deployment (Zero Downtime) / deploy-dev (push) Has been cancelled
fix: Decode HTML entities in chat responses and improve n8n error handling
- 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
2026-01-09 14:52:26 +01:00

213 lines
7.6 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { decodeHtmlEntitiesServer } from "@/lib/html-decode";
export async function POST(request: NextRequest) {
let userMessage = "";
try {
// Rate limiting for n8n chat endpoint
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown';
const { checkRateLimit } = await import('@/lib/auth');
if (!checkRateLimit(ip, 20, 60000)) { // 20 requests per minute for chat
return NextResponse.json(
{ error: 'Rate limit exceeded. Please try again later.' },
{ status: 429 }
);
}
const json = await request.json();
userMessage = json.message;
const history = json.history || [];
if (!userMessage || typeof userMessage !== "string") {
return NextResponse.json(
{ error: "Message is required" },
{ status: 400 },
);
}
// Call your n8n chat webhook
const n8nWebhookUrl = process.env.N8N_WEBHOOK_URL;
if (!n8nWebhookUrl) {
console.error("N8N_WEBHOOK_URL not configured");
return NextResponse.json({
reply: getFallbackResponse(userMessage),
});
}
const webhookUrl = `${n8nWebhookUrl}/webhook/chat`;
console.log(`Sending to n8n: ${webhookUrl}`);
// 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,
}),
signal: controller.signal,
});
clearTimeout(timeoutId);
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 ||
data.message ||
data.response ||
data.text ||
data.content ||
(Array.isArray(data) && data[0]?.reply);
if (!reply) {
console.warn("n8n response missing reply field:", data);
// If n8n returns successfully but without a clear reply field,
// we might want to show the fallback or a generic error,
// but strictly speaking we shouldn't show "Couldn't process".
// Let's try to stringify the whole data if it's small, or use fallback.
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) {
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");
}
// 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
return NextResponse.json({ reply: getFallbackResponse(userMessage) });
}
}
function getFallbackResponse(message: string): string {
if (!message || typeof message !== "string") {
return "I'm having a bit of trouble understanding. Could you try asking again?";
}
const lowerMessage = message.toLowerCase();
if (
lowerMessage.includes("skill") ||
lowerMessage.includes("tech") ||
lowerMessage.includes("stack")
) {
return "I specialize in full-stack development with Next.js, React, and Flutter for mobile. On the DevOps side, I love working with Docker Swarm, Traefik, and CI/CD pipelines. Basically, if it involves code or servers, I'm interested!";
}
if (
lowerMessage.includes("project") ||
lowerMessage.includes("built") ||
lowerMessage.includes("work")
) {
return "One of my key projects is Clarity, a Flutter app designed to help people with dyslexia. I also maintain a comprehensive self-hosted infrastructure with Docker Swarm. You can check out more details in the Projects section!";
}
if (
lowerMessage.includes("contact") ||
lowerMessage.includes("email") ||
lowerMessage.includes("reach") ||
lowerMessage.includes("hire")
) {
return "The best way to reach me is through the contact form below or by emailing contact@dk0.dev. I'm always open to discussing new ideas, opportunities, or just chatting about tech!";
}
if (
lowerMessage.includes("location") ||
lowerMessage.includes("where") ||
lowerMessage.includes("live")
) {
return "I'm based in Osnabrück, Germany. It's a great place to be a student and work on tech projects!";
}
if (
lowerMessage.includes("hobby") ||
lowerMessage.includes("free time") ||
lowerMessage.includes("fun")
) {
return "When I'm not coding or tweaking my servers, I enjoy gaming, going for a jog, or experimenting with new tech. Fun fact: I still use pen and paper for my calendar, even though I automate everything else!";
}
if (
lowerMessage.includes("devops") ||
lowerMessage.includes("docker") ||
lowerMessage.includes("server") ||
lowerMessage.includes("hosting")
) {
return "I'm really into DevOps! I run my own infrastructure on IONOS and OVHcloud using Docker Swarm and Traefik. It allows me to host various services and game servers efficiently while learning a ton about system administration.";
}
if (
lowerMessage.includes("student") ||
lowerMessage.includes("study") ||
lowerMessage.includes("education")
) {
return "Yes, I'm currently a student in Osnabrück. I balance my studies with working on personal projects and managing my self-hosted infrastructure. It keeps me busy but I learn something new every day!";
}
if (
lowerMessage.includes("hello") ||
lowerMessage.includes("hi ") ||
lowerMessage.includes("hey")
) {
return "Hi there! I'm Dennis's AI assistant (currently in offline mode). How can I help you learn more about Dennis today?";
}
// Default response
return "That's an interesting question! I'm currently operating in fallback mode, so my knowledge is a bit limited right now. But I can tell you that Dennis is a full-stack developer and DevOps enthusiast who loves building with Next.js and Docker. Feel free to ask about his skills, projects, or how to contact him!";
}