mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: upscale times out behind Cloudflare Tunnel due to blocking HTTP request
The upscale route held the HTTP connection open for the full duration of Python sidecar processing (30-300s). Behind proxies with connection timeouts (Cloudflare Tunnel: 100s), this caused HTTP 524 errors. The route now returns 202 Accepted immediately after upload validation and processes in the background. The result (downloadUrl, sizes, etc.) is delivered via the existing SSE progress channel. The frontend detects the 202 and waits for the SSE completion event instead of reading the XHR response body. A reconnect-safe completion store ensures results survive brief SSE disconnects. Closes #106
This commit is contained in:
@@ -94,6 +94,7 @@ export function useToolProcessor(toolId: string) {
|
||||
|
||||
// Generate client job ID for SSE correlation
|
||||
const clientJobId = generateId();
|
||||
let asyncMode = false;
|
||||
|
||||
// For AI tools, open SSE before uploading
|
||||
if (isAiTool) {
|
||||
@@ -104,8 +105,42 @@ export function useToolProcessor(toolId: string) {
|
||||
es.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === "single" && typeof data.percent === "number") {
|
||||
// Scale server progress (0-100) into 15-100 range
|
||||
if (data.type !== "single") return;
|
||||
|
||||
if (data.phase === "complete" && data.result) {
|
||||
if (elapsedRef.current) clearInterval(elapsedRef.current);
|
||||
if (processingTimerRef.current) clearInterval(processingTimerRef.current);
|
||||
es.close();
|
||||
eventSourceRef.current = null;
|
||||
|
||||
const result = data.result as ProcessResult;
|
||||
setWarning(result.warning ?? null);
|
||||
useFileStore.getState().updateEntry(capturedIndex, {
|
||||
processedUrl: result.downloadUrl,
|
||||
processedPreviewUrl: result.previewUrl ?? null,
|
||||
processedFilename: null,
|
||||
status: "completed",
|
||||
originalSize: result.originalSize,
|
||||
processedSize: result.processedSize,
|
||||
...(result.savedFileId ? { serverFileId: result.savedFileId } : {}),
|
||||
});
|
||||
setProcessing(false);
|
||||
setProgress(IDLE_PROGRESS);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.phase === "failed" && asyncMode) {
|
||||
if (elapsedRef.current) clearInterval(elapsedRef.current);
|
||||
if (processingTimerRef.current) clearInterval(processingTimerRef.current);
|
||||
es.close();
|
||||
eventSourceRef.current = null;
|
||||
setError(data.error || "Processing failed");
|
||||
setProcessing(false);
|
||||
setProgress(IDLE_PROGRESS);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof data.percent === "number") {
|
||||
const scaled = 15 + (data.percent / 100) * 85;
|
||||
setProgress((prev) => ({
|
||||
...prev,
|
||||
@@ -120,11 +155,13 @@ export function useToolProcessor(toolId: string) {
|
||||
};
|
||||
|
||||
es.onerror = () => {
|
||||
es.close();
|
||||
eventSourceRef.current = null;
|
||||
if (!asyncMode) {
|
||||
es.close();
|
||||
eventSourceRef.current = null;
|
||||
}
|
||||
};
|
||||
} catch {
|
||||
// EventSource creation failed — proceed without SSE
|
||||
// EventSource creation failed -- proceed without SSE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,6 +246,11 @@ export function useToolProcessor(toolId: string) {
|
||||
};
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === 202) {
|
||||
asyncMode = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (elapsedRef.current) clearInterval(elapsedRef.current);
|
||||
if (processingTimerRef.current) clearInterval(processingTimerRef.current);
|
||||
if (eventSourceRef.current) {
|
||||
@@ -220,8 +262,6 @@ export function useToolProcessor(toolId: string) {
|
||||
try {
|
||||
const result: ProcessResult = JSON.parse(xhr.responseText);
|
||||
setWarning(result.warning ?? null);
|
||||
// Write result to the entry that was being processed (captured at
|
||||
// request time), not whatever entry happens to be selected now.
|
||||
useFileStore.getState().updateEntry(capturedIndex, {
|
||||
processedUrl: result.downloadUrl,
|
||||
processedPreviewUrl: result.previewUrl ?? null,
|
||||
|
||||
Reference in New Issue
Block a user