fix(path-animator,image-picker): await submit POST before closing modal (v2.4.0)

Race condition: Continue/Cancel button handlers were firing submitBlocking()
without awaiting it, then immediately calling close() which tore down the
modal. If the fetch got delayed or dropped by the DOM teardown, the Python
thread would stay blocked on event.wait() until the full timeout.

Symptoms: "sometimes Continue gets stuck and doesn't advance the workflow."

Fixes:
- PathAnimator: All exit paths (Continue, Cancel, ESC, overlay click, header
  close) now await submitBlocking() before close(). On Continue failure,
  the modal stays open with buttons re-enabled so user can retry.
- PathAnimator: submitBlocking now checks response.ok and throws on non-2xx,
  surfacing 404 "Session not found" errors instead of swallowing them.
- PathAnimator: close() safeguard still uses fire-and-forget but now with
  .catch() to log errors (it's a last-resort path after primary handlers).
- ImagePicker: sendSelection now checks response.ok and throws on non-2xx.
- ImagePicker: All callers wrapped in try/catch so failed submits don't
  leave the modal in a half-closed state. Confirm failures keep the modal
  open with a user-visible alert.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Fillip
2026-04-10 17:27:02 -07:00
co-authored by Claude Opus 4.6
parent c05813d7bc
commit fa28ec8ec7
3 changed files with 116 additions and 49 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui_fill-nodes"
description = "Fill-Nodes is a versatile collection of custom nodes for ComfyUI that extends functionality across multiple domains. Features include advanced image processing (pixelation, slicing, masking), visual effects generation (glitch, halftone, pixel art), comprehensive file handling (PDF creation/extraction, Google Drive integration), AI model interfaces (GPT, DALL-E, Hugging Face), utility nodes for workflow enhancement, and specialized tools for video processing, captioning, and batch operations. The pack provides both practical workflow solutions and creative tools within a unified node collection."
version = "2.3.9"
version = "2.4.0"
license = "LICENSE"
dependencies = ["librosa", "sounddevice", "glitch_this", "PyOpenGL", "glfw", "scipy>=1.13.1", "requests", "aiohttp", "moviepy", "matplotlib", "reportlab", "openai", "PyPDF2", "pdf2image", "PyMuPDF", "reportlab", "PyPDF2", "ollama", "kornia", "opencv-python", "gdown", "open_clip_torch", "google-genai"]
+47 -23
View File
@@ -190,7 +190,11 @@ function showImagePickerModal(sessionId, images, batchSize, timeoutSeconds) {
clearInterval(countdownInterval);
countdownInterval = null;
// Timeout - cancel the job instead of sending images through
await sendSelection(sessionId, [], true);
try {
await sendSelection(sessionId, [], true);
} catch (err) {
console.error('[FL_ImagePicker] Timeout cancel failed:', err);
}
closeModal();
}
}, 1000);
@@ -434,13 +438,23 @@ function showImagePickerModal(sessionId, images, batchSize, timeoutSeconds) {
`;
const cancelBtn = createButton('Cancel', '#d9534f', async () => {
await sendSelection(sessionId, [], true);
try {
await sendSelection(sessionId, [], true);
} catch (e) {
console.error('[FL_ImagePicker] Cancel submit failed:', e);
}
closeModal();
});
const confirmBtn = createButton('Continue with Selected', '#4ECDC4', async () => {
const selection = Array.from(selectedIndices).sort((a, b) => a - b);
await sendSelection(sessionId, selection, false);
try {
await sendSelection(sessionId, selection, false);
} catch (e) {
console.error('[FL_ImagePicker] Confirm submit failed:', e);
alert('Failed to submit selection to backend. Check console for details.');
return; // Don't close — let user retry
}
closeModal();
}, true);
@@ -470,12 +484,22 @@ function showImagePickerModal(sessionId, images, batchSize, timeoutSeconds) {
if (e.key === 'Escape') {
e.preventDefault();
await sendSelection(sessionId, [], true);
try {
await sendSelection(sessionId, [], true);
} catch (err) {
console.error('[FL_ImagePicker] ESC cancel failed:', err);
}
closeModal();
} else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
const selection = Array.from(selectedIndices).sort((a, b) => a - b);
await sendSelection(sessionId, selection, false);
try {
await sendSelection(sessionId, selection, false);
} catch (err) {
console.error('[FL_ImagePicker] Ctrl+Enter confirm failed:', err);
alert('Failed to submit selection to backend. Check console for details.');
return;
}
closeModal();
}
};
@@ -532,25 +556,25 @@ function createButton(text, color, onClick, isPrimary = false) {
async function sendSelection(sessionId, selection, cancelled) {
console.log(`[FL_ImagePicker] Sending selection: session=${sessionId}, selection=${JSON.stringify(selection)}, cancelled=${cancelled}`);
try {
const response = await fetch('/fl_image_picker/select', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
session_id: sessionId,
selection: selection,
cancelled: cancelled
})
});
const result = await response.json();
console.log('[FL_ImagePicker] Selection sent successfully:', result);
return result;
} catch (error) {
console.error('[FL_ImagePicker] Error sending selection:', error);
throw error;
const response = await fetch('/fl_image_picker/select', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
session_id: sessionId,
selection: selection,
cancelled: cancelled
})
});
if (!response.ok) {
const msg = `[FL_ImagePicker] Selection POST failed with status ${response.status}`;
console.error(msg);
throw new Error(msg);
}
const result = await response.json();
console.log('[FL_ImagePicker] Selection sent successfully:', result);
return result;
}
// Full-resolution preview modal
+68 -25
View File
@@ -279,20 +279,24 @@ class PathEditorModal {
}
async submitBlocking(pathsJson, cancelled) {
try {
await fetch('/fl_path_animator/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: this.sessionId,
paths_data: pathsJson,
cancelled: cancelled,
}),
});
console.log(`[FL_PathAnimator] Submitted session ${this.sessionId} (cancelled=${cancelled})`);
} catch (e) {
console.error('[FL_PathAnimator] Submit failed:', e);
console.log(`[FL_PathAnimator] Submitting session ${this.sessionId} (cancelled=${cancelled})`);
const response = await fetch('/fl_path_animator/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: this.sessionId,
paths_data: pathsJson,
cancelled: cancelled,
}),
});
if (!response.ok) {
const msg = `Submit failed with status ${response.status}`;
console.error(`[FL_PathAnimator] ${msg}`);
throw new Error(msg);
}
const result = await response.json();
console.log(`[FL_PathAnimator] Submit OK:`, result);
return result;
}
refreshFooterForMode() {
@@ -343,11 +347,18 @@ class PathEditorModal {
if (e.key === 'Escape') {
if (this.mode === 'blocking' && !this.submitted) {
this.submitted = true;
this.submitBlocking(null, true);
(async () => {
try {
await this.submitBlocking(null, true);
} catch (err) {
console.error('[FL_PathAnimator] ESC cancel failed:', err);
}
this.close();
})();
} else {
this.savePaths();
this.close();
}
this.close();
}
// Ctrl+V to paste image from clipboard
@@ -579,11 +590,15 @@ class PathEditorModal {
this.overlay.appendChild(this.container);
// Close on overlay click (cancel in blocking mode)
this.overlay.addEventListener('click', (e) => {
this.overlay.addEventListener('click', async (e) => {
if (e.target === this.overlay) {
if (this.mode === 'blocking' && !this.submitted) {
this.submitted = true;
this.submitBlocking(null, true);
try {
await this.submitBlocking(null, true);
} catch (err) {
console.error('[FL_PathAnimator] Overlay click cancel failed:', err);
}
}
this.close();
}
@@ -675,10 +690,14 @@ class PathEditorModal {
closeBtn.style.borderColor = 'rgba(255, 255, 255, 0.1)';
closeBtn.style.transform = 'scale(1)';
};
closeBtn.onclick = () => {
closeBtn.onclick = async () => {
if (this.mode === 'blocking' && !this.submitted) {
this.submitted = true;
this.submitBlocking(null, true);
try {
await this.submitBlocking(null, true);
} catch (err) {
console.error('[FL_PathAnimator] Header close cancel failed:', err);
}
}
this.close();
};
@@ -1940,10 +1959,16 @@ class PathEditorModal {
cursor: pointer;
font-size: 14px;
`;
cancelBtn.onclick = () => {
cancelBtn.onclick = async () => {
if (this.mode === 'blocking' && !this.submitted) {
this.submitted = true;
this.submitBlocking(null, true);
cancelBtn.disabled = true;
saveBtn.disabled = true;
try {
await this.submitBlocking(null, true);
} catch (e) {
console.error('[FL_PathAnimator] Cancel submit failed:', e);
}
}
this.close();
};
@@ -1960,11 +1985,25 @@ class PathEditorModal {
font-size: 14px;
font-weight: 500;
`;
saveBtn.onclick = () => {
saveBtn.onclick = async () => {
this.savePaths();
if (this.mode === 'blocking' && !this.submitted) {
this.submitted = true;
this.submitBlocking(this.pathsDataWidget.value, false);
cancelBtn.disabled = true;
saveBtn.disabled = true;
saveBtn.textContent = 'Sending...';
try {
await this.submitBlocking(this.pathsDataWidget.value, false);
} catch (e) {
console.error('[FL_PathAnimator] Continue submit failed:', e);
// Re-enable so user can retry or cancel
this.submitted = false;
cancelBtn.disabled = false;
saveBtn.disabled = false;
saveBtn.textContent = 'Continue';
alert('Failed to submit paths to backend. Check console for details.');
return;
}
}
this.close();
};
@@ -2018,10 +2057,14 @@ class PathEditorModal {
close() {
// Safeguard: if blocking mode and we haven't submitted yet, cancel the session
// so the Python thread never permanently blocks
// so the Python thread never permanently blocks. Note: this is fire-and-forget
// because close() is synchronous — all primary exit paths (Continue, Cancel, ESC,
// overlay-click, header-close) already await submitBlocking before calling close().
if (this.mode === 'blocking' && !this.submitted) {
this.submitted = true;
this.submitBlocking(null, true);
this.submitBlocking(null, true).catch(err => {
console.error('[FL_PathAnimator] Safeguard cancel failed:', err);
});
}
// Clear countdown timer