laurent22/joplin · error · Error

Could not download from ${modelUrl}: Error ${response.status

Error message

Could not download from ${modelUrl}: Error ${response.status}

What it means

Thrown by `VoiceTyping.download()` when `shim.fetchBlob` returns a non-OK or 4xx/5xx response for the provider's model download URL. Used to abort the model install before attempting unzip/move. The URL and HTTP status are interpolated so the failure source is identifiable.

Source

Thrown at packages/app-mobile/services/voiceTyping/VoiceTyping.ts:107

	public async clearDownloads() {
		await this.provider.deleteCachedModels(this.locale);
	}

	public async download() {
		const modelPath = this.getModelPath();
		const modelUrl = this.provider.getDownloadUrl(this.locale);

		await shim.fsDriver().remove(modelPath);
		logger.info(`Downloading model from: ${modelUrl}`);

		const isZipped = modelUrl.endsWith('.zip');
		const downloadPath = isZipped ? `${modelPath}.zip` : modelPath;
		const response = await shim.fetchBlob(modelUrl, {
			path: downloadPath,
		});

		if (!response.ok || response.status >= 400) throw new Error(`Could not download from ${modelUrl}: Error ${response.status}`);

		if (isZipped) {
			const modelName = this.provider.modelName;
			const unzipDir = `${shim.fsDriver().getCacheDirectoryPath()}/voice-typing-extract/${modelName}/${this.locale}`;
			try {
				logger.info(`Unzipping ${downloadPath} => ${unzipDir}`);

				await unzip(downloadPath, unzipDir);

				const contents = await shim.fsDriver().readDirStats(unzipDir);
				if (contents.length !== 1) {
					logger.error('Expected 1 file or directory but got', contents);
					throw new Error(`Expected 1 file or directory, but got ${contents.length}`);
				}

				const fullUnzipPath = `${unzipDir}/${contents[0].path}`;

				logger.info(`Moving ${fullUnzipPath} => ${modelPath}`);

View on GitHub (pinned to 2654b33620)

Solutions

  1. Check network connectivity and retry the download (transient 5xx/timeout).
  2. Verify the provider's `getDownloadUrl(locale)` returns a live URL for the requested locale.
  3. Update the app / provider config to point at the current model distribution URL.
  4. Catch the error, surface it to the user, and offer a retry with exponential backoff.

Example fix

// before
const response = await shim.fetchBlob(modelUrl, { path: downloadPath });
if (!response.ok || response.status >= 400) throw new Error(`Could not download from ${modelUrl}: Error ${response.status}`);

// after
const response = await shim.fetchBlob(modelUrl, { path: downloadPath });
if (!response.ok || response.status >= 400) {
  throw new Error(`Could not download from ${modelUrl}: Error ${response.status}`);
}
// wrap caller in retry-on-5xx
Defensive patterns

Strategy: retry

Validate before calling

null

Type guard

null

Try / catch

const downloadWithRetry = async (url, downloadPath, attempts = 3) => {
  for (let i = 0; i < attempts; i++) {
    const response = await shim.fetchBlob(url, { path: downloadPath });
    if (response.ok && response.status < 400) return response;
    if (response.status < 500) break; // do not retry 4xx
    await new Promise(r => setTimeout(r, 2 ** i * 500));
  }
  throw new Error(`Could not download from ${url}`);
};

Prevention

When it happens

Trigger: The provider's `getDownloadUrl(locale)` returns a URL that 404s, the host is unreachable (network/DNS), the CDN returns 5xx, or a redirect was not followed. Also when `response.ok` is false for any reason.

Common situations: Model URL changed upstream but the bundled provider still points at the old path; offline or metered connection drops mid-download; regional CDN block; the locale has no published model file.

Related errors


AI-assisted analysis of laurent22/joplin@2654b33620 (2026-08-12). Data as JSON: /api/errors/a86fcdd05407fcf2. Report an issue: GitHub.