diff --git a/web/src/api/chats.ts b/web/src/api/chats.ts index d65c6e6..cfec78f 100644 --- a/web/src/api/chats.ts +++ b/web/src/api/chats.ts @@ -104,17 +104,32 @@ export async function uploadToPresignedUrl( uploadUrl: string, requiredHeaders: Record, file: File, - onProgress?: (percent: number) => void + onProgress?: (percent: number) => void, + retries = 2 ): Promise { - await axios.put(uploadUrl, file, { - headers: requiredHeaders, - onUploadProgress: (progressEvent) => { - if (!onProgress || !progressEvent.total) { - return; + let attempt = 0; + while (attempt <= retries) { + try { + await axios.put(uploadUrl, file, { + headers: requiredHeaders, + onUploadProgress: (progressEvent) => { + if (!onProgress || !progressEvent.total) { + return; + } + onProgress(Math.round((progressEvent.loaded * 100) / progressEvent.total)); + } + }); + return; + } catch (error) { + const status = axios.isAxiosError(error) ? error.response?.status : undefined; + const retryable = status === undefined || status >= 500; + if (!retryable || attempt >= retries) { + throw error; } - onProgress(Math.round((progressEvent.loaded * 100) / progressEvent.total)); + attempt += 1; + await new Promise((resolve) => window.setTimeout(resolve, 400 * attempt)); } - }); + } } export async function attachFile(messageId: number, fileUrl: string, fileType: string, fileSize: number): Promise { diff --git a/web/src/components/MessageComposer.tsx b/web/src/components/MessageComposer.tsx index 7a20f06..320c145 100644 --- a/web/src/components/MessageComposer.tsx +++ b/web/src/components/MessageComposer.tsx @@ -77,7 +77,21 @@ export function MessageComposer() { const replyToMessageId = activeChatId ? (replyToByChat[activeChatId]?.id ?? undefined) : undefined; addOptimisticMessage({ chatId: activeChatId, senderId: me.id, type: "text", text: textValue, clientMessageId }); try { - const message = await sendMessageWithClientId(activeChatId, textValue, "text", clientMessageId, replyToMessageId); + let message = null; + for (let attempt = 0; attempt < 2; attempt += 1) { + try { + message = await sendMessageWithClientId(activeChatId, textValue, "text", clientMessageId, replyToMessageId); + break; + } catch (error) { + if (attempt === 1) { + throw error; + } + await new Promise((resolve) => window.setTimeout(resolve, 250)); + } + } + if (!message) { + throw new Error("send failed"); + } confirmMessageByClientId(activeChatId, clientMessageId, message); setText(""); clearDraft(activeChatId); @@ -110,8 +124,12 @@ export function MessageComposer() { clientMessageId }); const message = await sendMessageWithClientId(activeChatId, upload.file_url, messageType, clientMessageId, replyToMessageId); - await attachFile(message.id, upload.file_url, file.type || "application/octet-stream", file.size); confirmMessageByClientId(activeChatId, clientMessageId, message); + try { + await attachFile(message.id, upload.file_url, file.type || "application/octet-stream", file.size); + } catch { + setUploadError("File sent, but metadata save failed. Please refresh chat."); + } setReplyToMessage(activeChatId, null); } catch { removeOptimisticMessage(activeChatId, clientMessageId);