actualbudget/actual · error · FileUploadError

internal

internal

Error message

internal

What it means

FileUploadError('internal') is thrown when the upload POST itself throws but the error is not a PostError — i.e., an unexpected exception (fetch transport crash, serialization bug, unhandled promise rejection) rather than a structured server response. It is a catch-all for non-classified upload exceptions.

Source

Thrown at packages/loot-core/src/server/cloud-storage.ts:363

          : null),
        ...(groupId ? { 'X-ACTUAL-GROUP-ID': groupId } : null),
        // TODO: fix me
        // oxlint-disable-next-line typescript/no-explicit-any
      },
      body: uploadContent,
    });
  } catch (err) {
    logger.log('Upload failure', err);

    if (err instanceof PostError) {
      throw FileUploadError(
        err.reason === 'unauthorized'
          ? 'unauthorized'
          : err.reason || 'network',
      );
    }

    throw FileUploadError('internal');
  }

  if (res.status === 'ok') {
    // Only save it if we are still working on the same file
    if (prefs.getPrefs() && prefs.getPrefs().id === id) {
      await prefs.savePrefs({
        lastUploaded: monthUtils.currentDay(),
        cloudFileId,
        groupId: res.groupId,
      });
    }
  } else {
    throw FileUploadError('internal');
  }
}

export async function possiblyUpload() {
  const { cloudFileId, groupId, lastUploaded } = prefs.getPrefs();

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Check the app logs: 'Upload failure' is logged with the original error before this is thrown — fix the underlying cause
  2. Retry the upload; transient transport errors often resolve
  3. Verify the server URL configuration (serverURL) is a valid absolute URL
  4. Update Actual — newer versions classify more failure modes properly

Example fix

// before: opaque internal failure
await upload();
// after: capture underlying cause via logs and retry with backoff
for (let i = 0; i < 3; i++) {
  try { await upload(); break; }
  catch (e) { if (e.reason !== 'internal') throw e; await delay(1000 * 2 ** i); }
}
Defensive patterns

Strategy: retry

Validate before calling

function assertValidServerUrl(url) {
  const u = new URL(url);
  if (!/^https?:$/.test(u.protocol)) throw new Error(`invalid server URL: ${url}`);
  return url;
}

Type guard

function isInternalUploadError(e) {
  return e instanceof FileUploadError && e.reason === 'internal';
}

Try / catch

try {
  await upload();
} catch (e) {
  if (isInternalUploadError(e)) {
    logger.error('upload internal failure — check underlying cause in logs', e);
    await backoffRetry(upload, 2); // may be transient transport issue
  } else throw e;
}

Prevention

When it happens

Trigger: the fetchJSON call in upload() throws an exception that is not an instance of PostError — e.g. an abrupt network socket error, request construction failure, or an error thrown inside header/body preparation.

Common situations: proxy or TLS middleware throwing, undici/node fetch low-level failures, misconfigured server URL producing malformed requests, memory pressure with very large budgets.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/a92f091d186b8bba. Report an issue: GitHub.