Skip to content

Commit 6558a80

Browse files
authored
Fix UI timeouts (#494)
Signed-off-by: Mihai Criveti <[email protected]>
1 parent d32b3e7 commit 6558a80

File tree

1 file changed

+124
-45
lines changed

1 file changed

+124
-45
lines changed

mcpgateway/static/admin.js

Lines changed: 124 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,8 @@ function isInactiveChecked(type) {
177177
}
178178

179179
// Enhanced fetch with timeout and better error handling
180-
function fetchWithTimeout(url, options = {}, timeout = 10000) {
180+
function fetchWithTimeout(url, options = {}, timeout = 30000) {
181+
// Increased from 10000
181182
const controller = new AbortController();
182183
const timeoutId = setTimeout(() => {
183184
console.warn(`Request to ${url} timed out after ${timeout}ms`);
@@ -196,32 +197,41 @@ function fetchWithTimeout(url, options = {}, timeout = 10000) {
196197
})
197198
.then((response) => {
198199
clearTimeout(timeoutId);
199-
if (
200-
response.status === 0 ||
201-
(response.ok && response.status === 200)
202-
) {
200+
201+
// FIX: Better handling of empty responses
202+
if (response.status === 0) {
203+
// Status 0 often indicates a network error or CORS issue
204+
throw new Error(
205+
"Network error or server is not responding. Please ensure the server is running and accessible.",
206+
);
207+
}
208+
209+
if (response.ok && response.status === 200) {
203210
const contentLength = response.headers.get("content-length");
204211

205212
// Check Content-Length if present
206-
if (contentLength !== null) {
207-
if (parseInt(contentLength, 10) === 0) {
208-
throw new Error(
209-
"Server returned an empty response (via header)",
210-
);
211-
}
212-
} else {
213-
// Fallback: check actual body
214-
const cloned = response.clone();
215-
return cloned.text().then((text) => {
216-
if (!text.trim()) {
217-
throw new Error(
218-
"Server returned an empty response (via body)",
219-
);
220-
}
221-
return response;
222-
});
213+
if (
214+
contentLength !== null &&
215+
parseInt(contentLength, 10) === 0
216+
) {
217+
console.warn(
218+
`Empty response from ${url} (Content-Length: 0)`,
219+
);
220+
// Don't throw error for intentionally empty responses
221+
return response;
223222
}
223+
224+
// For responses without Content-Length, clone and check
225+
const cloned = response.clone();
226+
return cloned.text().then((text) => {
227+
if (!text || !text.trim()) {
228+
console.warn(`Empty response body from ${url}`);
229+
// Return the original response anyway
230+
}
231+
return response;
232+
});
224233
}
234+
225235
return response;
226236
})
227237
.catch((error) => {
@@ -230,15 +240,21 @@ function fetchWithTimeout(url, options = {}, timeout = 10000) {
230240
// Improve error messages for common issues
231241
if (error.name === "AbortError") {
232242
throw new Error(
233-
`Request timed out after ${timeout / 1000} seconds`,
243+
`Request timed out after ${timeout / 1000} seconds. The server may be slow or unresponsive.`,
234244
);
235-
} else if (error.message.includes("Failed to fetch")) {
245+
} else if (
246+
error.message.includes("Failed to fetch") ||
247+
error.message.includes("NetworkError")
248+
) {
236249
throw new Error(
237-
"Unable to connect to server. Please check if the server is running.",
250+
"Unable to connect to server. Please check if the server is running on the correct port.",
238251
);
239-
} else if (error.message.includes("empty response")) {
252+
} else if (
253+
error.message.includes("empty response") ||
254+
error.message.includes("ERR_EMPTY_RESPONSE")
255+
) {
240256
throw new Error(
241-
"Server returned an empty response. The endpoint may not be implemented.",
257+
"Server returned an empty response. This endpoint may not be implemented yet or the server crashed.",
242258
);
243259
}
244260

@@ -510,8 +526,8 @@ function resetModalState(modalId) {
510526
// More robust metrics request tracking
511527
let metricsRequestController = null;
512528
let metricsRequestPromise = null;
513-
const MAX_METRICS_RETRIES = 2; // Reduced from 3
514-
const METRICS_RETRY_DELAY = 1500; // Reduced from 2000ms
529+
const MAX_METRICS_RETRIES = 3; // Increased from 2
530+
const METRICS_RETRY_DELAY = 2000; // Increased from 1500ms
515531

516532
/**
517533
* Enhanced metrics loading with better race condition prevention
@@ -548,7 +564,7 @@ async function loadMetricsInternal() {
548564
const result = await fetchWithTimeoutAndRetry(
549565
`${window.ROOT_PATH}/admin/metrics`,
550566
{}, // options
551-
20000, // 20 second timeout (reduced from 30)
567+
45000, // Increased timeout specifically for metrics (was 20000)
552568
MAX_METRICS_RETRIES,
553569
);
554570

@@ -558,10 +574,30 @@ async function loadMetricsInternal() {
558574
showMetricsPlaceholder();
559575
return;
560576
}
577+
// FIX: Handle 500 errors specifically
578+
if (result.status >= 500) {
579+
throw new Error(
580+
`Server error (${result.status}). The metrics calculation may have failed.`,
581+
);
582+
}
561583
throw new Error(`HTTP ${result.status}: ${result.statusText}`);
562584
}
563585

564-
const data = await result.json();
586+
// FIX: Handle empty or invalid JSON responses
587+
let data;
588+
try {
589+
const text = await result.text();
590+
if (!text || !text.trim()) {
591+
console.warn("Empty metrics response, using default data");
592+
data = {}; // Use empty object as fallback
593+
} else {
594+
data = JSON.parse(text);
595+
}
596+
} catch (parseError) {
597+
console.error("Failed to parse metrics JSON:", parseError);
598+
data = {}; // Use empty object as fallback
599+
}
600+
565601
displayMetrics(data);
566602
console.log("✓ Metrics loaded successfully");
567603
} catch (error) {
@@ -745,6 +781,25 @@ function displayMetrics(data) {
745781
}
746782

747783
try {
784+
// FIX: Handle completely empty data
785+
if (!data || Object.keys(data).length === 0) {
786+
const emptyStateDiv = document.createElement("div");
787+
emptyStateDiv.className = "text-center p-8 text-gray-500";
788+
emptyStateDiv.innerHTML = `
789+
<svg class="mx-auto h-12 w-12 text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
790+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
791+
</svg>
792+
<h3 class="text-lg font-medium mb-2">No Metrics Available</h3>
793+
<p class="text-sm">Metrics data will appear here once tools, resources, or prompts are executed.</p>
794+
<button onclick="retryLoadMetrics()" class="mt-4 bg-indigo-600 text-white px-4 py-2 rounded hover:bg-indigo-700 transition-colors">
795+
Refresh Metrics
796+
</button>
797+
`;
798+
metricsPanel.innerHTML = "";
799+
metricsPanel.appendChild(emptyStateDiv);
800+
return;
801+
}
802+
748803
// Create main container with safe structure
749804
const mainContainer = document.createElement("div");
750805
mainContainer.className = "space-y-6";
@@ -2863,8 +2918,8 @@ function handleSubmitWithConfirmation(event, type) {
28632918
const toolTestState = {
28642919
activeRequests: new Map(), // toolId -> AbortController
28652920
lastRequestTime: new Map(), // toolId -> timestamp
2866-
debounceDelay: 500, // ms
2867-
requestTimeout: 10000, // Reduced from 15000ms
2921+
debounceDelay: 1000, // Increased from 500ms
2922+
requestTimeout: 30000, // Increased from 10000ms
28682923
};
28692924

28702925
/**
@@ -2878,7 +2933,7 @@ async function testTool(toolId) {
28782933
const now = Date.now();
28792934
const lastRequest = toolTestState.lastRequestTime.get(toolId) || 0;
28802935
const timeSinceLastRequest = now - lastRequest;
2881-
const enhancedDebounceDelay = 1000; // Increased from 500ms
2936+
const enhancedDebounceDelay = 2000; // Increased from 1000ms
28822937

28832938
if (timeSinceLastRequest < enhancedDebounceDelay) {
28842939
console.log(
@@ -2888,7 +2943,7 @@ async function testTool(toolId) {
28882943
(enhancedDebounceDelay - timeSinceLastRequest) / 1000,
28892944
);
28902945
showErrorMessage(
2891-
`Please wait ${waitTime} more seconds before testing again`,
2946+
`Please wait ${waitTime} more second${waitTime > 1 ? "s" : ""} before testing again`,
28922947
);
28932948
return;
28942949
}
@@ -2928,7 +2983,7 @@ async function testTool(toolId) {
29282983
toolTestState.activeRequests.set(toolId, controller);
29292984
toolTestState.lastRequestTime.set(toolId, now);
29302985

2931-
// 6. MAKE REQUEST with increased timeout (was 10 seconds, now 15)
2986+
// 6. MAKE REQUEST with increased timeout
29322987
const response = await fetchWithTimeout(
29332988
`${window.ROOT_PATH}/admin/tools/${toolId}`,
29342989
{
@@ -2938,7 +2993,7 @@ async function testTool(toolId) {
29382993
Pragma: "no-cache",
29392994
},
29402995
},
2941-
15000, // Increased timeout
2996+
toolTestState.requestTimeout, // Use the increased timeout
29422997
);
29432998

29442999
if (!response.ok) {
@@ -3165,7 +3220,7 @@ async function runToolTest() {
31653220
params,
31663221
};
31673222

3168-
// Use shorter timeout for test execution
3223+
// Use longer timeout for test execution
31693224
const response = await fetchWithTimeout(
31703225
`${window.ROOT_PATH}/rpc`,
31713226
{
@@ -3176,8 +3231,8 @@ async function runToolTest() {
31763231
body: JSON.stringify(payload),
31773232
credentials: "include",
31783233
},
3179-
8000,
3180-
); // 8 second timeout
3234+
20000, // Increased from 8000
3235+
);
31813236

31823237
const result = await response.json();
31833238
const resultStr = JSON.stringify(result, null, 2);
@@ -4183,6 +4238,7 @@ function setupTooltipsWithAlpine() {
41834238

41844239
Alpine.directive("tooltip", (el, { expression }, { evaluate }) => {
41854240
let tooltipEl = null;
4241+
let animationFrameId = null; // Track animation frame
41864242

41874243
const moveTooltip = (e) => {
41884244
if (!tooltipEl) {
@@ -4234,9 +4290,19 @@ function setupTooltipsWithAlpine() {
42344290
tooltipEl.style.top = `${rect.bottom + scrollY + 10}px`;
42354291
}
42364292

4237-
requestAnimationFrame(() => {
4238-
tooltipEl.style.opacity = "1";
4293+
// FIX: Cancel any pending animation frame before setting a new one
4294+
if (animationFrameId) {
4295+
cancelAnimationFrame(animationFrameId);
4296+
}
4297+
4298+
animationFrameId = requestAnimationFrame(() => {
4299+
// FIX: Check if tooltipEl still exists before accessing its style
4300+
if (tooltipEl) {
4301+
tooltipEl.style.opacity = "1";
4302+
}
4303+
animationFrameId = null;
42394304
});
4305+
42404306
window.addEventListener("scroll", hideTooltip, {
42414307
passive: true,
42424308
});
@@ -4250,15 +4316,28 @@ function setupTooltipsWithAlpine() {
42504316
return;
42514317
}
42524318

4319+
// FIX: Cancel any pending animation frame
4320+
if (animationFrameId) {
4321+
cancelAnimationFrame(animationFrameId);
4322+
animationFrameId = null;
4323+
}
4324+
42534325
tooltipEl.style.opacity = "0";
42544326
el.removeEventListener("mousemove", moveTooltip);
42554327
window.removeEventListener("scroll", hideTooltip);
42564328
window.removeEventListener("resize", hideTooltip);
4257-
el.addEventListener("click", hideTooltip);
4329+
el.removeEventListener("click", hideTooltip);
4330+
42584331
const toRemove = tooltipEl;
4259-
tooltipEl = null;
4260-
setTimeout(() => toRemove.remove(), 200);
4332+
tooltipEl = null; // Set to null immediately
4333+
4334+
setTimeout(() => {
4335+
if (toRemove && toRemove.parentNode) {
4336+
toRemove.parentNode.removeChild(toRemove);
4337+
}
4338+
}, 200);
42614339
};
4340+
42624341
el.addEventListener("mouseenter", showTooltip);
42634342
el.addEventListener("mouseleave", hideTooltip);
42644343
el.addEventListener("focus", showTooltip);

0 commit comments

Comments
 (0)