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);
} catch (error) {
// 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
const decryptPromise = (async () => {
try {
setIsLoading(true);
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) {
throw new Error(`Failed to decrypt: ${response.statusText}`);
@ -52,6 +60,11 @@ export const useDecryptContent = () => {
return decryptedContent;
} catch (error) {
// Handle abort errors specifically
if (axios.isCancel(error)) {
throw new DOMException('Decryption aborted', 'AbortError');
}
setError(error.message || 'Decryption failed');
// Re-throw to signal failure to awaiter
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);
// Function to handle timeouts from parent callers
decryptPromise.cancel = () => {
abortController.abort();
};
// Return the promise
try {
return await decryptPromise;

View File

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