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

  1. Retry the sync — if another client deleted the item, the next delta will see it as gone and reconcile.
  2. Check the sync target for orphaned/partial files (e.g. zero-byte objects on S3, empty files on WebDAV) and remove them.
  3. Verify the sync target is healthy and not returning empty bodies under load.
  4. 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

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


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