cancel in-flight decryption requests on timeout

This commit is contained in:
austinkelsay 2025-05-10 17:04:39 -05:00
parent f0f5b54768
commit 1e9e9471b7
No known key found for this signature in database
GPG Key ID: 5A763922E5BA08EE
2 changed files with 43 additions and 7 deletions

View File

@ -29,17 +29,25 @@ export const useDecryptContent = () => {
return await inProgressMap.current.get(cacheKey); return await inProgressMap.current.get(cacheKey);
} catch (error) { } catch (error) {
// If the existing promise rejects, we'll try again below // If the existing promise rejects, we'll try again below
console.warn('Previous decryption attempt failed, retrying'); if (error.name !== 'AbortError') {
console.warn('Previous decryption attempt failed, retrying');
}
} }
} }
// Create abort controller for this request
const abortController = new AbortController();
// Create a new decryption promise for this content // Create a new decryption promise for this content
const decryptPromise = (async () => { const decryptPromise = (async () => {
try { try {
setIsLoading(true); setIsLoading(true);
setError(null); setError(null);
const response = await axios.post('/api/decrypt', { encryptedContent }); const response = await axios.post('/api/decrypt',
{ encryptedContent },
{ signal: abortController.signal }
);
if (response.status !== 200) { if (response.status !== 200) {
throw new Error(`Failed to decrypt: ${response.statusText}`); throw new Error(`Failed to decrypt: ${response.statusText}`);
@ -52,6 +60,11 @@ export const useDecryptContent = () => {
return decryptedContent; return decryptedContent;
} catch (error) { } catch (error) {
// Handle abort errors specifically
if (axios.isCancel(error)) {
throw new DOMException('Decryption aborted', 'AbortError');
}
setError(error.message || 'Decryption failed'); setError(error.message || 'Decryption failed');
// Re-throw to signal failure to awaiter // Re-throw to signal failure to awaiter
throw error; throw error;
@ -62,9 +75,19 @@ export const useDecryptContent = () => {
} }
})(); })();
// Store the promise in our map // Store the promise and abort controller in our map
const abortablePromise = {
promise: decryptPromise,
abort: () => abortController.abort()
};
inProgressMap.current.set(cacheKey, decryptPromise); inProgressMap.current.set(cacheKey, decryptPromise);
// Function to handle timeouts from parent callers
decryptPromise.cancel = () => {
abortController.abort();
};
// Return the promise // Return the promise
try { try {
return await decryptPromise; return await decryptPromise;

View File

@ -196,19 +196,32 @@ const useDecryption = (session, paidCourse, course, lessons, setLessons, router)
processingRef.current = true; processingRef.current = true;
setLoading(true); setLoading(true);
// Start the decryption process
const decryptionPromise = decryptContent(currentLesson.content);
// Add safety timeout to prevent infinite processing // Add safety timeout to prevent infinite processing
const timeoutPromise = new Promise((_, reject) => let timeoutId;
setTimeout(() => reject(new Error('Decryption timeout')), 10000) const timeoutPromise = new Promise((_, reject) => {
); timeoutId = setTimeout(() => {
// Cancel the in-flight request when timeout occurs
if (decryptionPromise.cancel) {
decryptionPromise.cancel();
}
reject(new Error('Decryption timeout'));
}, 10000);
});
// Use a separate try-catch for the race // Use a separate try-catch for the race
let decryptedContent; let decryptedContent;
try { try {
// Race between decryption and timeout // Race between decryption and timeout
decryptedContent = await Promise.race([ decryptedContent = await Promise.race([
decryptContent(currentLesson.content), decryptionPromise,
timeoutPromise timeoutPromise
]); ]);
// Clear the timeout if decryption wins
clearTimeout(timeoutId);
} catch (error) { } catch (error) {
// If timeout or network error, schedule a retry // If timeout or network error, schedule a retry
setTimeout(() => { setTimeout(() => {