laurent22/joplin · error · Error
Got metadata for path but could not fetch content: ${path}
Error message
Got metadata for path but could not fetch content: ${path} What it means
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.
Source
Thrown at packages/lib/Synchronizer.ts:701
}
} else {
// Note: in order to know the real updated_time value, we need to load the content. In theory we could
// rely on the file timestamp (in remote.updated_time) but in practice it's not accurate enough and
// can lead to conflicts (for example when the file timestamp is slightly ahead of its real
// updated_time). updated_time is set and managed by clients so it's always accurate.
// Same situation below for updateLocal.
//
// This is a bit inefficient because if the resulting action is "updateRemote" we don't need the whole
// content, but for now that will do since being reliable is the priority.
//
// Note: assuming a particular sync target is guaranteed to have accurate timestamps, the driver maybe
// could expose this with a accurateTimestamps() method that returns "true". In that case, the test
// could be done using the file timestamp and the potentially unnecessary content loading could be skipped.
// OneDrive does not appear to have accurate timestamps as lastModifiedDateTime would occasionally be
// a few seconds ahead of what it was set with setTimestamp()
try {
remoteContent = await this.apiCall('get', path);
if (!remoteContent) throw new Error(`Got metadata for path but could not fetch content: ${path}`);
remoteContent = await BaseItem.unserialize(remoteContent);
} catch (error) {
if (error.code === 'rejectedByTarget' || error.code === 'malformedItem') {
this.progressReport_.errors.push(error);
logger.warn(`Skipping item from sync target: ${path}: ${error.message}`);
completeItemProcessing(path);
continue;
} else {
throw error;
}
}
if (remoteContent.updated_time > local.sync_time) {
// Since, in this loop, we are only dealing with items that require sync, if the
// remote has been modified after the sync time, it means both items have been
// modified and so there's a conflict.
action = getConflictType(local);
reason = 'both remote and local have changes';View on GitHub (pinned to 2654b33620)
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.
Defensive patterns
Strategy: try-catch
Validate before calling
// After stat, sanity-check that the remote item is still fetchable
// (best-effort; the real guard is in the try/catch below)
const remote = await synchronizer.apiCall('stat', path);
if (remote && !remote.updated_time) {
// stat returned a stub; content fetch may come back empty
logger.warn('Remote stat has no updated_time for', path);
} Try / catch
try {
const remoteContent = await synchronizer.apiCall('get', path);
if (!remoteContent) throw new Error(`Got metadata for path but could not fetch content: ${path}`);
} catch (error) {
if (error.code === 'rejectedByTarget' || error.code === 'malformedItem') {
// skip this item, log, and continue the sync
continue;
}
throw error;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Cannot initialise synchroniser.
- changedDuringSync
- No item with ID ${itemId}
- Please specify the sync target path.
- Synchronisation is already in progress.
AI-assisted analysis of laurent22/joplin@2654b33620 (2026-08-12).
Data as JSON: /api/errors/d67f3382d1a69ded.
Report an issue: GitHub.