{"record":{"id":"9fdeb3ec38e7a23a","repo":"laurent22/joplin","slug":"fetchblob-method-url-error-tostring","errorCode":null,"errorMessage":"fetchBlob: ${method} ${url}: ${error.toString()}","messagePattern":"fetchBlob: (.+?) (.+?): (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/app-mobile/utils/shim-init-react/index.ts","lineNumber":130,"sourceCode":"\t\t\t// Returns an object that's roughly compatible with a standard Response object\n\t\t\tconst output = {\n\t\t\t\tok: response.respInfo.status < 400,\n\t\t\t\tpath: response.data,\n\t\t\t\tstatus: response.respInfo.status,\n\t\t\t\theaders: response.respInfo.headers,\n\t\t\t\t// If response type is 'path' then calling text() or json() (or base64())\n\t\t\t\t// on RNFetchBlob response object will make it read the file on the native thread,\n\t\t\t\t// serialize it, and send over the RN bridge.\n\t\t\t\t// For larger files this can cause the app to crash.\n\t\t\t\t// For these type of responses we're not using the response text anyway\n\t\t\t\t// so can override it here to return empty values\n\t\t\t\ttext: response.type === 'path' ? () => '' : response.text,\n\t\t\t\tjson: response.type === 'path' ? () => {} : response.json,\n\t\t\t};\n\n\t\t\treturn output;\n\t\t} catch (error) {\n\t\t\tthrow new Error(`fetchBlob: ${method} ${url}: ${error.toString()}`);\n\t\t}\n\t};\n\n\tshim.uploadBlob = async function(url, options) {\n\t\tif (!options || !options.path) throw new Error('uploadBlob: source file path is missing');\n\n\t\tconst headers = options.headers ? options.headers : {};\n\t\tconst method = options.method ? options.method : 'POST';\n\n\t\ttry {\n\t\t\tconst response = await RNFetchBlob.config({\n\t\t\t\ttrusty: options.ignoreTlsErrors,\n\t\t\t}).fetch(method, url, headers, RNFetchBlob.wrap(options.path));\n\n\t\t\t// Returns an object that's roughly compatible with a standard Response object\n\t\t\treturn {\n\t\t\t\tok: response.respInfo.status < 400,\n\t\t\t\tdata: response.data,","sourceCodeStart":112,"sourceCodeEnd":148,"githubUrl":"https://github.com/laurent22/joplin/blob/2654b33620775080d1d59c552259d41e33dad3d2/packages/app-mobile/utils/shim-init-react/index.ts#L112-L148","documentation":"Wraps any exception thrown by the RNFetchBlob-backed mobile download path (shim.fetchBlob). The catch on line 129 re-throws a generic Error whose message is 'fetchBlob: <method> <url>: <original error>'. The original structured error is flattened via error.toString(), so the caller receives only a string and loses the RNFetchBlob error object and status code.","triggerScenarios":"Network unreachable (airplane mode, no connectivity); invalid/unreachable URL; TLS handshake failure (self-signed cert, expired cert) when ignoreTlsErrors is false; RNFetchBlob native module misconfigured; the server returns an error that RNFetchBlob surfaces as a rejection; insufficient storage at localFilePath.","commonSituations":"Syncing on a flaky mobile network; downloading a resource attachment behind a self-signed HTTPS endpoint; RNFetchBlob/Expo upgrade broke the native fetch bridge; target path points to a non-writable directory; large file download interrupted.","solutions":["Check connectivity and retry — shim.fetchWithRetry already wraps the call, so a persistent failure means a hard network or server problem.","If the endpoint uses self-signed TLS, pass options.ignoreTlsErrors = true (dev only).","Verify localFilePath is under a writable RNFetchBlob.fs.dirs directory and the device has free space.","Reproduce with shim.debugFetch(url) (defined in index.ts) to get the raw XHR response for diagnosis.","Upgrade/reinstall rn-fetch-blob (or its maintained fork react-native-blob-util) to match the RN version."],"exampleFix":"// before\nawait shim.fetchBlob(url, { path, method: 'GET' });\n// after — add resilience and TLS override for known self-signed hosts\ntry {\n\tawait shim.fetchBlob(url, { path, method: 'GET', ignoreTlsErrors: isDev });\n} catch (e) {\n\tlogger.error('Download failed', e.message);\n\tthrow e;\n}","handlingStrategy":"retry","validationCode":"if (!url || typeof url !== 'string') throw new Error('fetchBlob requires a URL string');\ntry { new URL(url); } catch { throw new Error(`Invalid URL: ${url}`); }\nif (!(await shim.fsDriver().exists(dirname(localFilePath)))) await shim.fsDriver().mkdir(dirname(localFilePath));\nawait shim.fetchBlob(url, { path: localFilePath, method: options.method ?? 'GET' });","typeGuard":"function hasValidPath(options) { return !!options && typeof options.path === 'string' && options.path.length > 0; }","tryCatchPattern":"try {\n  await shim.fetchBlob(url, { path, method: 'GET' });\n} catch (e) {\n  if (/Network|timeout|ETIMEDOUT/i.test(e.message)) {\n    await backoffRetry(() => shim.fetchBlob(url, { path, method: 'GET' }));\n  } else throw e;\n}","preventionTips":["Validate the URL with new URL() before downloading.","Ensure the target directory is writable and disk has space.","Use shim.debugFetch to diagnose opaque RNFetchBlob errors.","Pass ignoreTlsErrors only in dev for self-signed endpoints."],"tags":["mobile","network","rn-fetch-blob","download","tls"],"backgroundTag":null,"analyzedSha":"2654b33620775080d1d59c552259d41e33dad3d2","analyzedAt":"2026-08-12T14:26:46.263Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}