zhlajiex
commited on
Commit
·
afc2629
1
Parent(s):
3185ed5
Fix: Resolve 'Link Severed' race condition by preventing double-termination of streams and improving error handling
Browse files- backend/controllers/ai.js +19 -5
- backend/public/chat.html +6 -0
backend/controllers/ai.js
CHANGED
|
@@ -181,6 +181,7 @@ exports.chat = asyncHandler(async (req, res, next) => {
|
|
| 181 |
}
|
| 182 |
} catch (e) { console.error("Gradio Error", e); }
|
| 183 |
|
|
|
|
| 184 |
await Message.create({ sessionId: session._id, sender: 'user', content: message || "[SIGNAL]", attachmentUrl });
|
| 185 |
await Message.create({ sessionId: session._id, sender: 'ai', content: fullAIResponse || "[EMPTY_SIGNAL]", modelUsed: activeModelName });
|
| 186 |
user.usage.requestsToday += 1;
|
|
@@ -248,13 +249,15 @@ exports.chat = asyncHandler(async (req, res, next) => {
|
|
| 248 |
console.log(`[DEBUG] Calling API: ${apiUrl} for model: ${apiModelId}`); const response = await axios.post(apiUrl, { model: apiModelId, messages: apiMessages, stream: true, temperature: 0.7 }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, responseType: 'stream' });
|
| 249 |
|
| 250 |
let fullAIResponse = "";
|
|
|
|
|
|
|
| 251 |
response.data.on('data', chunk => {
|
| 252 |
const lines = chunk.toString().split('\n');
|
| 253 |
for (const line of lines) {
|
| 254 |
if (line.startsWith('data: ')) {
|
| 255 |
-
const dataStr = line.slice(6);
|
| 256 |
if (dataStr === '[DONE]') {
|
| 257 |
-
|
| 258 |
return;
|
| 259 |
}
|
| 260 |
try {
|
|
@@ -270,6 +273,7 @@ exports.chat = asyncHandler(async (req, res, next) => {
|
|
| 270 |
});
|
| 271 |
|
| 272 |
response.data.on('end', async () => {
|
|
|
|
| 273 |
await Message.create({ sessionId: session._id, sender: 'user', content: message || "[SIGNAL]", attachmentUrl });
|
| 274 |
await Message.create({ sessionId: session._id, sender: 'ai', content: fullAIResponse, modelUsed: activeModelName });
|
| 275 |
user.usage.requestsToday += 1;
|
|
@@ -278,11 +282,21 @@ exports.chat = asyncHandler(async (req, res, next) => {
|
|
| 278 |
res.end();
|
| 279 |
});
|
| 280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
} catch (err) {
|
| 282 |
console.error("[DEBUG] Chat Controller Catch:", err.message);
|
| 283 |
-
res.
|
| 284 |
-
|
| 285 |
-
|
|
|
|
|
|
|
| 286 |
}
|
| 287 |
});
|
| 288 |
|
|
|
|
| 181 |
}
|
| 182 |
} catch (e) { console.error("Gradio Error", e); }
|
| 183 |
|
| 184 |
+
if (res.writableEnded) return;
|
| 185 |
await Message.create({ sessionId: session._id, sender: 'user', content: message || "[SIGNAL]", attachmentUrl });
|
| 186 |
await Message.create({ sessionId: session._id, sender: 'ai', content: fullAIResponse || "[EMPTY_SIGNAL]", modelUsed: activeModelName });
|
| 187 |
user.usage.requestsToday += 1;
|
|
|
|
| 249 |
console.log(`[DEBUG] Calling API: ${apiUrl} for model: ${apiModelId}`); const response = await axios.post(apiUrl, { model: apiModelId, messages: apiMessages, stream: true, temperature: 0.7 }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, responseType: 'stream' });
|
| 250 |
|
| 251 |
let fullAIResponse = "";
|
| 252 |
+
let isStreamEnded = false;
|
| 253 |
+
|
| 254 |
response.data.on('data', chunk => {
|
| 255 |
const lines = chunk.toString().split('\n');
|
| 256 |
for (const line of lines) {
|
| 257 |
if (line.startsWith('data: ')) {
|
| 258 |
+
const dataStr = line.slice(6).trim();
|
| 259 |
if (dataStr === '[DONE]') {
|
| 260 |
+
isStreamEnded = true;
|
| 261 |
return;
|
| 262 |
}
|
| 263 |
try {
|
|
|
|
| 273 |
});
|
| 274 |
|
| 275 |
response.data.on('end', async () => {
|
| 276 |
+
if (res.writableEnded) return;
|
| 277 |
await Message.create({ sessionId: session._id, sender: 'user', content: message || "[SIGNAL]", attachmentUrl });
|
| 278 |
await Message.create({ sessionId: session._id, sender: 'ai', content: fullAIResponse, modelUsed: activeModelName });
|
| 279 |
user.usage.requestsToday += 1;
|
|
|
|
| 282 |
res.end();
|
| 283 |
});
|
| 284 |
|
| 285 |
+
response.data.on('error', (err) => {
|
| 286 |
+
console.error("Stream Error:", err);
|
| 287 |
+
if (!res.writableEnded) {
|
| 288 |
+
res.write(`data: ${JSON.stringify({ error: "STREAM_ERROR", details: err.message })}\n\n`);
|
| 289 |
+
res.end();
|
| 290 |
+
}
|
| 291 |
+
});
|
| 292 |
+
|
| 293 |
} catch (err) {
|
| 294 |
console.error("[DEBUG] Chat Controller Catch:", err.message);
|
| 295 |
+
if (!res.writableEnded) {
|
| 296 |
+
res.write(`data: ${JSON.stringify({ error: "NEURAL_LINK_SEVERED", details: err.message })}\n\n`);
|
| 297 |
+
res.write(`data: ${JSON.stringify({ done: true })}\n\n`);
|
| 298 |
+
res.end();
|
| 299 |
+
}
|
| 300 |
}
|
| 301 |
});
|
| 302 |
|
backend/public/chat.html
CHANGED
|
@@ -397,6 +397,12 @@
|
|
| 397 |
if (line.startsWith('data: ')) {
|
| 398 |
try {
|
| 399 |
const data = JSON.parse(line.slice(6));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
if (data.message) {
|
| 401 |
fullText += data.message;
|
| 402 |
aiNode.querySelector('.prose').innerHTML = marked.parse(fullText);
|
|
|
|
| 397 |
if (line.startsWith('data: ')) {
|
| 398 |
try {
|
| 399 |
const data = JSON.parse(line.slice(6));
|
| 400 |
+
if (data.error) {
|
| 401 |
+
aiNode.querySelector('.prose').innerHTML += `<div class="text-red-500 font-bold mt-2">!! ${data.error}: ${data.details || ''}</div>`;
|
| 402 |
+
isProcessing = false;
|
| 403 |
+
clearTimeout(safetyTimeout);
|
| 404 |
+
return;
|
| 405 |
+
}
|
| 406 |
if (data.message) {
|
| 407 |
fullText += data.message;
|
| 408 |
aiNode.querySelector('.prose').innerHTML = marked.parse(fullText);
|