Skip to content

Fix: terminal Muapi errors are retried for 30 minutes instead of failing fast - #314

Open
alnavone wants to merge 1 commit into
Anil-matcha:mainfrom
alnavone:fix/muapi-poll-fatal-errors
Open

Fix: terminal Muapi errors are retried for 30 minutes instead of failing fast#314
alnavone wants to merge 1 commit into
Anil-matcha:mainfrom
alnavone:fix/muapi-poll-fatal-errors

Conversation

@alnavone

@alnavone alnavone commented Aug 8, 2026

Copy link
Copy Markdown

Fix: terminal Muapi errors are retried for 30 minutes instead of failing fast

A generation that has already failed server-side — out of credits, rate limited, bad request — is indistinguishable from one still running, for up to half an hour. The UI shows a spinner the whole time and never surfaces an error.

Found while debugging a video generation that appeared to hang for 15+ minutes. The job had actually been rejected with a 400 almost immediately.

The bug

pollForResult throws on a fatal HTTP status and on an explicit failed job status. Both throws are inside the try block, so the loop's own catch swallows them and keeps polling:

if (!response.ok) {
    const errText = await response.text();
    if (response.status >= 500) continue;
    throw new Error(`Poll Failed: ${response.status} - ...`);   // thrown inside try…
}
const data = await response.json();
const status = data.status?.toLowerCase();
if (status === 'completed' || ...) return data;
if (status === 'failed' || status === 'error') {
    throw new Error(`Generation failed: ${data.error || 'Unknown error'}`);   // …and this one too
}
} catch (error) {
    if (attempt === maxAttempts) throw error;                    // …caught right here
    console.warn('[Muapi] Poll attempt failed, retrying...', error.message);
}

The if (response.status >= 500) continue guard shows the intent was to treat 5xx as transient and anything else as fatal. The control flow never did that.

Impact scales with maxAttempts:

Caller maxAttempts Interval Time spent retrying a dead job
generateImage 60 2s 2 minutes
generateVideo 900 2s 30 minutes

A 402 (out of credits), 429 (rate limited), 401, or 400 gets retried 900 times. So does a job the server has explicitly reported as failed.

The fix

Tag terminal errors and rethrow them from the catch:

function fatal(message) {
    const error = new Error(message);
    error.isFatal = true;
    return error;
}
} catch (error) {
    if (error.isFatal || attempt === maxAttempts) throw error;
    console.warn('[Muapi] Poll attempt failed, retrying...', error.message);
}

5xx responses and network errors stay retryable — those genuinely are transient.

Applied to all three polling loops, since the same pattern is duplicated:

  • src/lib/muapi.jspollForResult (Electron renderer)
  • packages/studio/src/muapi.jspollForResult (web studio)
  • packages/studio/src/muapi.jspollWorkflowResult

Tests

tests/muapiPolling.test.js, matching the existing node:test suite. It extracts the real pollForResult from src/lib/muapi.js and runs it against a stubbed fetch, so it tests the shipped code rather than a copy of the logic. Extraction is brace-matched, not offset-based, so it survives edits to the surrounding file.

  • a 402 stops after exactly one request
  • a failed job status stops after exactly one request
  • 503 is retried, then succeeds
  • pending keeps polling until the job completes
  • network errors remain retryable and use every attempt
node --test "tests/**/*.test.js"
# tests 22 | pass 22 | fail 0

17 pre-existing tests still pass.

Note

This is independent of the Windows CUDA / sd.cpp packaging fixes I opened separately — different subsystem, different failure mode. The two branches touch no common files and can be reviewed and merged in either order.

pollForResult throws on a 4xx response and on an explicit "failed" job
status, but both throws are inside the try block, so the loop's own catch
swallows them and keeps polling. Video generation polls 900 times at 2s
intervals, so an out-of-credits, rate-limited, or already-failed job is
indistinguishable from one still running for the full 30 minutes -- the UI
just shows a spinner. The `if (response.status >= 500) continue` guard shows
the intent was to fail fast on 4xx; the control flow never did.

Tag terminal errors with isFatal and rethrow them from the catch. 5xx and
network errors stay retryable.

Fixed in all three polling loops:
  - src/lib/muapi.js          pollForResult (Electron renderer)
  - packages/studio/src/muapi.js  pollForResult (web studio)
  - packages/studio/src/muapi.js  pollWorkflowResult

Adds tests/muapiPolling.test.js, which extracts the shipped pollForResult
and asserts a 402 and a failed status each stop after exactly one request,
while 503, pending, and network errors still retry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant