{"record":{"id":"d67f3382d1a69ded","repo":"laurent22/joplin","slug":"got-metadata-for-path-but-could-not-fetch-content","errorCode":null,"errorMessage":"Got metadata for path but could not fetch content: ${path}","messagePattern":"Got metadata for path but could not fetch content: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/lib/Synchronizer.ts","lineNumber":701,"sourceCode":"\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Note: in order to know the real updated_time value, we need to load the content. In theory we could\n\t\t\t\t\t\t\t// rely on the file timestamp (in remote.updated_time) but in practice it's not accurate enough and\n\t\t\t\t\t\t\t// can lead to conflicts (for example when the file timestamp is slightly ahead of its real\n\t\t\t\t\t\t\t// updated_time). updated_time is set and managed by clients so it's always accurate.\n\t\t\t\t\t\t\t// Same situation below for updateLocal.\n\t\t\t\t\t\t\t//\n\t\t\t\t\t\t\t// This is a bit inefficient because if the resulting action is \"updateRemote\" we don't need the whole\n\t\t\t\t\t\t\t// content, but for now that will do since being reliable is the priority.\n\t\t\t\t\t\t\t//\n\t\t\t\t\t\t\t// Note: assuming a particular sync target is guaranteed to have accurate timestamps, the driver maybe\n\t\t\t\t\t\t\t// could expose this with a accurateTimestamps() method that returns \"true\". In that case, the test\n\t\t\t\t\t\t\t// could be done using the file timestamp and the potentially unnecessary content loading could be skipped.\n\t\t\t\t\t\t\t// OneDrive does not appear to have accurate timestamps as lastModifiedDateTime would occasionally be\n\t\t\t\t\t\t\t// a few seconds ahead of what it was set with setTimestamp()\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tremoteContent = await this.apiCall('get', path);\n\t\t\t\t\t\t\t\tif (!remoteContent) throw new Error(`Got metadata for path but could not fetch content: ${path}`);\n\t\t\t\t\t\t\t\tremoteContent = await BaseItem.unserialize(remoteContent);\n\t\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\t\tif (error.code === 'rejectedByTarget' || error.code === 'malformedItem') {\n\t\t\t\t\t\t\t\t\tthis.progressReport_.errors.push(error);\n\t\t\t\t\t\t\t\t\tlogger.warn(`Skipping item from sync target: ${path}: ${error.message}`);\n\t\t\t\t\t\t\t\t\tcompleteItemProcessing(path);\n\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tthrow error;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (remoteContent.updated_time > local.sync_time) {\n\t\t\t\t\t\t\t\t// Since, in this loop, we are only dealing with items that require sync, if the\n\t\t\t\t\t\t\t\t// remote has been modified after the sync time, it means both items have been\n\t\t\t\t\t\t\t\t// modified and so there's a conflict.\n\t\t\t\t\t\t\t\taction = getConflictType(local);\n\t\t\t\t\t\t\t\treason = 'both remote and local have changes';","sourceCodeStart":683,"sourceCodeEnd":719,"githubUrl":"https://github.com/laurent22/joplin/blob/2654b33620775080d1d59c552259d41e33dad3d2/packages/lib/Synchronizer.ts#L683-L719","documentation":"Thrown after a successful apiCall('get', path) returns a falsy value (null/undefined/empty). The sync had just obtained metadata (the 'stat' that produced the remote item), but the subsequent content fetch came back empty. This indicates the sync target returned a directory entry with no readable body — a partially-written file, a race where the item was deleted between stat and get, or a driver quirk.","triggerScenarios":"During DELTA, remote stat returns a valid RemoteItem, but the following apiCall('get', path) resolves to null/undefined. The guard fires before unserialize is attempted. If the underlying cause is a 'rejectedByTarget' or 'malformedItem' code, it is caught by the surrounding try/catch and skipped; otherwise this Error propagates.","commonSituations":"Item deleted on the sync target between the stat and the get (race with another client); a partially-uploaded file from an interrupted previous sync; WebDAV/S3 returning an empty body for a key that exists in listing; driver returning empty content on transient server errors.","solutions":["Retry the sync — if another client deleted the item, the next delta will see it as gone and reconcile.","Check the sync target for orphaned/partial files (e.g. zero-byte objects on S3, empty files on WebDAV) and remove them.","Verify the sync target is healthy and not returning empty bodies under load.","Inspect logs for the requestId/path to see if the driver logged a server-side error swallowed as an empty response."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// After stat, sanity-check that the remote item is still fetchable\n// (best-effort; the real guard is in the try/catch below)\nconst remote = await synchronizer.apiCall('stat', path);\nif (remote && !remote.updated_time) {\n  // stat returned a stub; content fetch may come back empty\n  logger.warn('Remote stat has no updated_time for', path);\n}","typeGuard":null,"tryCatchPattern":"try {\n  const remoteContent = await synchronizer.apiCall('get', path);\n  if (!remoteContent) throw new Error(`Got metadata for path but could not fetch content: ${path}`);\n} catch (error) {\n  if (error.code === 'rejectedByTarget' || error.code === 'malformedItem') {\n    // skip this item, log, and continue the sync\n    continue;\n  }\n  throw error;\n}","preventionTips":["Handle empty-content fetches by skipping the item and continuing the sync rather than aborting.","Periodically check the sync target for zero-byte/partial objects and remove them.","Retry the sync when another client may have deleted the item between stat and get."],"tags":["sync","content-fetch","race-condition","sync-target"],"backgroundTag":null,"analyzedSha":"2654b33620775080d1d59c552259d41e33dad3d2","analyzedAt":"2026-08-12T14:26:46.263Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}