Skip to content

Commit fa4701d

Browse files
authored
fix(UI-1709): fix timeout with chat in iframe (#1264)
1 parent b981c6d commit fa4701d

File tree

5 files changed

+248
-54
lines changed

5 files changed

+248
-54
lines changed

src/hooks/useChatbotIframe.ts

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { EventListenerName } from "@src/enums";
99
import { triggerEvent } from "@src/hooks";
1010

1111
export const useChatbotIframeConnection = (iframeRef: React.RefObject<HTMLIFrameElement>, onConnect?: () => void) => {
12-
const { t } = useTranslation("chatbot", { keyPrefix: "iframeComponent" });
12+
const { t } = useTranslation("chatbot");
1313

1414
const [isLoading, setIsLoading] = useState<boolean>(true);
1515
const [loadError, setLoadError] = useState<string | null>(null);
@@ -44,8 +44,8 @@ export const useChatbotIframeConnection = (iframeRef: React.RefObject<HTMLIFrame
4444

4545
const eventErrorDetail = detail || localizedBaseMessage;
4646
triggerEvent(EventListenerName.iframeError, {
47-
message: tRef.current("connectionError"),
48-
error: tRef.current("connectionErrorExtended", { error: eventErrorDetail }),
47+
message: tRef.current("iframeComponent.connectionError"),
48+
error: tRef.current("iframeComponent.connectionErrorExtended", { error: eventErrorDetail }),
4949
});
5050
}, []);
5151

@@ -65,22 +65,63 @@ export const useChatbotIframeConnection = (iframeRef: React.RefObject<HTMLIFrame
6565
let isMounted = true;
6666
const currentIframe = iframeRef.current;
6767
let timeoutId: number | undefined = undefined;
68+
let retryTimeoutId: number | undefined = undefined;
6869

6970
iframeCommService.setIframe(currentIframe);
7071

71-
const connectAsync = async () => {
72+
const connectionConfig = {
73+
maxRetries: 3,
74+
retryDelay: 2000,
75+
} as const;
76+
77+
const scheduleRetry = (reason: string, retryCount: number, errorType: string, errorDetail: string): boolean => {
78+
if (retryCount < connectionConfig.maxRetries && isMounted) {
79+
LoggerService.debug(
80+
namespaces.chatbot,
81+
t("errors.serverRespondedWithStatus", {
82+
reason,
83+
retryDelay: connectionConfig.retryDelay,
84+
currentAttempt: retryCount + 1,
85+
maxAttempts: connectionConfig.maxRetries + 1,
86+
})
87+
);
88+
retryTimeoutId = window.setTimeout(() => {
89+
if (isMounted) {
90+
connectAsync(retryCount + 1);
91+
}
92+
}, connectionConfig.retryDelay);
93+
return true;
94+
}
95+
96+
isConnectingRef.current = false;
97+
handleError(errorType, errorDetail);
98+
return false;
99+
};
100+
101+
const connectAsync = async (retryCount = 0) => {
72102
try {
73103
const response = await fetch(aiChatbotUrl, { method: "HEAD", credentials: "include" });
74104
if (!response.ok) {
75105
if (isMounted) {
76-
handleError("connectionRefused", `Server responded with status ${response.status}`);
106+
scheduleRetry(
107+
`Server responded with status ${response.status}`,
108+
retryCount,
109+
"connectionRefused",
110+
`Server responded with status ${response.status}`
111+
);
77112
}
78113
return;
79114
}
80115

81116
timeoutId = window.setTimeout(() => {
82117
if (isLoadingRef.current && isMounted) {
83-
handleError("connectionError", "Timeout waiting for iframe connection");
118+
if (timeoutId) clearTimeout(timeoutId);
119+
scheduleRetry(
120+
"Connection timeout",
121+
retryCount,
122+
"connectionError",
123+
"Timeout waiting for iframe connection"
124+
);
84125
}
85126
}, chatbotIframeConnectionTimeout);
86127

@@ -95,15 +136,13 @@ export const useChatbotIframeConnection = (iframeRef: React.RefObject<HTMLIFrame
95136
setIsRetryLoading(false);
96137
}, 1750);
97138
onConnectRef.current?.();
139+
isConnectingRef.current = false;
98140
}
99141
} catch (error) {
100142
if (timeoutId) clearTimeout(timeoutId);
101143
if (isMounted) {
102-
handleError("connectionError", error instanceof Error ? error.message : String(error));
103-
}
104-
} finally {
105-
if (isMounted) {
106-
isConnectingRef.current = false;
144+
const errorMessage = error instanceof Error ? error.message : String(error);
145+
scheduleRetry(`Connection error: ${errorMessage}`, retryCount, "connectionError", errorMessage);
107146
}
108147
}
109148
};
@@ -116,9 +155,12 @@ export const useChatbotIframeConnection = (iframeRef: React.RefObject<HTMLIFrame
116155
if (timeoutId) {
117156
clearTimeout(timeoutId);
118157
}
158+
if (retryTimeoutId) {
159+
clearTimeout(retryTimeoutId);
160+
}
119161
};
120162
// eslint-disable-next-line react-hooks/exhaustive-deps
121-
}, [iframeRef, isIframeElementLoaded]);
163+
}, [iframeRef, isIframeElementLoaded, t]);
122164

123165
const handleRetry = useCallback(() => {
124166
if (iframeRef.current) {
@@ -135,20 +177,21 @@ export const useChatbotIframeConnection = (iframeRef: React.RefObject<HTMLIFrame
135177
url.searchParams.set("retry", Date.now().toString());
136178
iframeRef.current.src = url.toString();
137179
} catch (error) {
138-
LoggerService.error(namespaces.chatbot, `Error setting iframe src: ${error}`);
180+
LoggerService.error(namespaces.chatbot, t("errors.errorSettingIframeSrc", { error }));
139181
}
140182

141183
setTimeout(() => {
142184
setIsRetryLoading(false);
143185
}, 1750);
144186
} else {
145-
LoggerService.error(namespaces.chatbot, "iframeRef.current is null, cannot retry");
187+
LoggerService.error(namespaces.chatbot, t("errors.iframeRefIsNull"));
146188
setTimeout(() => {
147189
setIsRetryLoading(false);
148190
}, 1750);
149191
}
150192
// eslint-disable-next-line react-hooks/exhaustive-deps
151-
}, [iframeRef, aiChatbotUrl]);
193+
}, [iframeRef, aiChatbotUrl, t]);
194+
152195
return {
153196
isLoading,
154197
loadError,

src/locales/en/chatbot/translation.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,10 @@
1717
"connectionRefused": "The AI assistant is not available at the moment. Please try again later.",
1818
"aiTitle": "AI Assistant",
1919
"configTitle": "Status"
20+
},
21+
"errors": {
22+
"serverRespondedWithStatus": "{{reason}}, retrying in {{retryDelay}}ms (attempt {{currentAttempt}}/{{maxAttempts}})",
23+
"errorSettingIframeSrc": "Error setting iframe src: {{error}}",
24+
"iframeRefIsNull": "iframeRef.current is null, cannot retry"
2025
}
2126
}

src/locales/en/services/translation.json

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,42 @@
118118
"errorRemovingUserToOrganization": "Removing user from organization failed",
119119
"errorRemovingUserToOrganizationExtended": "Removing user from organization failed, organization ID: {{organizationId}}, error: {{error}}",
120120
"errorUpdatingUserToOrganizationExtended": "Updating user to organization failed, organization ID: {{organizationId}}, error: {{error}}",
121-
"iframeComm": {
122-
"iframeReferenceNotSet": "Iframe reference not set",
123-
"noConnectionAttemptInProgress": "No connection attempt in progress",
124-
"iframeContentWindowNotAvailable": "Iframe contentWindow is not available",
125-
"requestTimeoutForResource": "Request timeout for resource: {{resource}}",
126-
"invalidMessageReceivedOrSourceMismatch": "Invalid message received or source mismatch: {{message}}",
127-
"errorProcessingIncomingMessage": "Error processing incoming message: {{error}}",
128-
"errorImportingStoreForFileContentHandling": "Error importing store for file content handling: {{error}}",
129-
"errorImportingStoreForDiagramDisplayHandling": "Error importing store for diagram display handling: {{error}}"
121+
"debug": {
122+
"iframeComm": {
123+
"settingIframeReference": "Setting iframe reference",
124+
"destroyingService": "Destroying service - cleaning up all resources",
125+
"resettingService": "Resetting service for navigation",
126+
"initiatingHandshake": "Initiating handshake with iframe",
127+
"handshakeMessageSent": "Handshake message sent",
128+
"alreadyConnected": "Already connected, returning",
129+
"noConnectionPromiseExists": "No connection promise exists, trying to initiate handshake",
130+
"waitingForConnection": "Waiting for connection promise to resolve",
131+
"messageQueueFull": "Message queue full ({{maxQueueSize}}), dropping oldest messages",
132+
"postingMessageToChatbot": "Posting message to chatbot at origin {{origin}}: {{message}}",
133+
"duplicateRequestForResource": "Duplicate request for resource {{resource}}, waiting for existing request",
134+
"invalidOrigin": "Invalid origin: {{origin}}, expected: {{expectedOrigin}}",
135+
"invalidMessageFormat": "Invalid message format",
136+
"failedToSendEvent": "Failed to send event {{eventName}} to chatbot iframe: {{error}}. This is expected if chatbot is not open.",
137+
"iframeRemovedFromDOM": "Iframe removed from DOM - resetting service"
138+
}
139+
},
140+
"errors": {
141+
"iframeComm": {
142+
"failedToSendHandshakeMessage": "Failed to send handshake message: {{error}}",
143+
"queueProcessingExceeded": "Queue processing exceeded max attempts ({{maxAttempts}}), clearing queue",
144+
"errorProcessingIncomingMessage": "Error processing incoming message: {{error}}",
145+
"errorImportingStoreForVarUpdatedHandling": "Error importing store for var updated handling: {{error}}",
146+
"failedToDownloadFile": "Failed to download file {{filename}}: {{error}}",
147+
"failedToDownloadChatFile": "Failed to download chat file {{filename}}: {{error}}",
148+
"iframeReferenceNotSet": "Iframe reference not set",
149+
"noConnectionAttemptInProgress": "No connection attempt in progress",
150+
"iframeContentWindowNotAvailable": "Iframe contentWindow is not available",
151+
"requestTimeoutForResource": "Request timeout for resource: {{resource}}",
152+
"invalidMessageReceivedOrSourceMismatch": "Invalid message received or source mismatch: {{message}}",
153+
"errorImportingStoreForFileContentHandling": "Error importing store for file content handling: {{error}}",
154+
"errorImportingStoreForDiagramDisplayHandling": "Error importing store for diagram display handling: {{error}}",
155+
"serviceDestroyedDuringNavigation": "Service destroyed during navigation",
156+
"serviceResetDuringNavigation": "Service reset during navigation"
157+
}
130158
}
131159
}

0 commit comments

Comments
 (0)