Skip to content

Commit e248a67

Browse files
frontend ui error configurable timeout (#57)
1 parent d0dca40 commit e248a67

File tree

2 files changed

+78
-12
lines changed

2 files changed

+78
-12
lines changed

frontend/src/pages/App.jsx

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import { apiService } from "../services/api";
66
const POLL_INTERVAL = 600; // 0.6 seconds
77
const INITIAL_ERROR_STATE = { visible: false, message: '' };
88
const DEBOUNCE_DELAY = 300; // 300ms debounce for user input
9+
const CONVERSATION_FETCH_ERROR_DELAY_MS = 10000; // wait 10s before showing fetch errors
10+
const CONVERSATION_FETCH_ERROR_THRESHOLD = Math.ceil(
11+
CONVERSATION_FETCH_ERROR_DELAY_MS / POLL_INTERVAL
12+
);
913

1014
function useDebounce(value, delay) {
1115
const [debouncedValue, setDebouncedValue] = useState(value);
@@ -39,28 +43,49 @@ export default function App() {
3943
const debouncedUserInput = useDebounce(userInput, DEBOUNCE_DELAY);
4044

4145
const errorTimerRef = useRef(null);
46+
const conversationFetchErrorCountRef = useRef(0);
4247

4348
const handleError = useCallback((error, context) => {
4449
console.error(`${context}:`, error);
45-
46-
const isConversationFetchError = error.status === 404;
47-
const errorMessage = isConversationFetchError
48-
? "Error fetching conversation. Retrying..." // Updated message
50+
51+
const isConversationFetchError =
52+
context === "fetching conversation" && (error.status === 404 || error.status === 408);
53+
54+
if (isConversationFetchError) {
55+
if (error.status === 404) {
56+
conversationFetchErrorCountRef.current += 1;
57+
58+
const hasExceededThreshold =
59+
conversationFetchErrorCountRef.current >= CONVERSATION_FETCH_ERROR_THRESHOLD;
60+
61+
if (!hasExceededThreshold) {
62+
return;
63+
}
64+
} else {
65+
// For timeouts or other connectivity errors surface immediately
66+
conversationFetchErrorCountRef.current = CONVERSATION_FETCH_ERROR_THRESHOLD;
67+
}
68+
} else {
69+
conversationFetchErrorCountRef.current = 0;
70+
}
71+
72+
const errorMessage = isConversationFetchError
73+
? "Error fetching conversation. Retrying..."
4974
: `Error ${context.toLowerCase()}. Please try again.`;
50-
75+
5176
setError(prevError => {
5277
// If the same 404 error is already being displayed, don't reset state (prevents flickering)
5378
if (prevError.visible && prevError.message === errorMessage) {
5479
return prevError;
5580
}
5681
return { visible: true, message: errorMessage };
5782
});
58-
83+
5984
// Clear any existing timeout
6085
if (errorTimerRef.current) {
6186
clearTimeout(errorTimerRef.current);
6287
}
63-
88+
6489
// Only auto-dismiss non-404 errors after 3 seconds
6590
if (!isConversationFetchError) {
6691
errorTimerRef.current = setTimeout(() => setError(INITIAL_ERROR_STATE), 3000);
@@ -72,6 +97,7 @@ export default function App() {
7297
if (errorTimerRef.current) {
7398
clearTimeout(errorTimerRef.current);
7499
}
100+
conversationFetchErrorCountRef.current = 0;
75101
setError(INITIAL_ERROR_STATE);
76102
}, []);
77103

frontend/src/services/api.js

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
const API_BASE_URL = 'http://127.0.0.1:8000';
22

3+
const resolveRequestTimeout = () => {
4+
const env = typeof import.meta !== 'undefined' ? import.meta.env : undefined;
5+
const configured = env?.VITE_API_TIMEOUT_MS;
6+
const parsed = Number.parseInt(configured, 10);
7+
if (Number.isFinite(parsed) && parsed > 0) {
8+
return parsed;
9+
}
10+
return 15000;
11+
};
12+
13+
const REQUEST_TIMEOUT_MS = resolveRequestTimeout(); // default to 15s, overridable via Vite env
14+
315
class ApiError extends Error {
416
constructor(message, status) {
517
super(message);
@@ -19,12 +31,31 @@ async function handleResponse(response) {
1931
return response.json();
2032
}
2133

34+
async function fetchWithTimeout(url, options = {}, timeout = REQUEST_TIMEOUT_MS) {
35+
const controller = new AbortController();
36+
const timeoutId = setTimeout(() => controller.abort(), timeout);
37+
38+
try {
39+
return await fetch(url, { ...options, signal: controller.signal });
40+
} catch (error) {
41+
if (error.name === 'AbortError') {
42+
throw new ApiError('Request timed out', 408);
43+
}
44+
throw error;
45+
} finally {
46+
clearTimeout(timeoutId);
47+
}
48+
}
49+
2250
export const apiService = {
2351
async getConversationHistory() {
2452
try {
25-
const res = await fetch(`${API_BASE_URL}/get-conversation-history`);
53+
const res = await fetchWithTimeout(`${API_BASE_URL}/get-conversation-history`);
2654
return handleResponse(res);
2755
} catch (error) {
56+
if (error instanceof ApiError) {
57+
throw error;
58+
}
2859
throw new ApiError(
2960
'Failed to fetch conversation history',
3061
error.status || 500
@@ -38,7 +69,7 @@ export const apiService = {
3869
}
3970

4071
try {
41-
const res = await fetch(
72+
const res = await fetchWithTimeout(
4273
`${API_BASE_URL}/send-prompt?prompt=${encodeURIComponent(message)}`,
4374
{
4475
method: 'POST',
@@ -49,6 +80,9 @@ export const apiService = {
4980
);
5081
return handleResponse(res);
5182
} catch (error) {
83+
if (error instanceof ApiError) {
84+
throw error;
85+
}
5286
throw new ApiError(
5387
'Failed to send message',
5488
error.status || 500
@@ -58,7 +92,7 @@ export const apiService = {
5892

5993
async startWorkflow() {
6094
try {
61-
const res = await fetch(
95+
const res = await fetchWithTimeout(
6296
`${API_BASE_URL}/start-workflow`,
6397
{
6498
method: 'POST',
@@ -69,6 +103,9 @@ export const apiService = {
69103
);
70104
return handleResponse(res);
71105
} catch (error) {
106+
if (error instanceof ApiError) {
107+
throw error;
108+
}
72109
throw new ApiError(
73110
'Failed to start workflow',
74111
error.status || 500
@@ -78,18 +115,21 @@ export const apiService = {
78115

79116
async confirm() {
80117
try {
81-
const res = await fetch(`${API_BASE_URL}/confirm`, {
118+
const res = await fetchWithTimeout(`${API_BASE_URL}/confirm`, {
82119
method: 'POST',
83120
headers: {
84121
'Content-Type': 'application/json'
85122
}
86123
});
87124
return handleResponse(res);
88125
} catch (error) {
126+
if (error instanceof ApiError) {
127+
throw error;
128+
}
89129
throw new ApiError(
90130
'Failed to confirm action',
91131
error.status || 500
92132
);
93133
}
94134
}
95-
};
135+
};

0 commit comments

Comments
 (0)