laurent22/joplin · error · Error

href ${href} not in baseUrl ${baseUrl} nor relativeBaseUrl $

Error message

href ${href} not in baseUrl ${baseUrl} nor relativeBaseUrl ${relativeBaseUrl}

What it means

Thrown by hrefToRelativePath_() when converting a PROPFIND href into a sync-relative path. It tries three matches: the href starts with baseUrl, with relativeBaseUrl, or their percent-decoded forms. If none match, the href doesn't belong to the configured sync root and the driver can't map it, so it aborts to avoid silently mis-addressing files.

Source

Thrown at packages/lib/file-api-driver-webdav.js:113

			return result.items;
		};

		return await basicDelta(path, getDirStats, options);
	}

	// A file href, as found in the result of a PROPFIND, can be either an absolute URL or a
	// relative URL (an absolute URL minus the protocol and domain), while the sync algorithm
	// works with paths relative to the base URL.
	hrefToRelativePath_(href, baseUrl, relativeBaseUrl) {
		let output = '';
		if (href.indexOf(baseUrl) === 0) {
			output = href.substr(baseUrl.length);
		} else if (href.indexOf(relativeBaseUrl) === 0) {
			output = href.substr(relativeBaseUrl.length);
		} else if (decodeURIComponent(href).indexOf(decodeURIComponent(relativeBaseUrl)) === 0) {
			output = decodeURIComponent(href).substring(decodeURIComponent(relativeBaseUrl).length);
		} else {
			throw new Error(`href ${href} not in baseUrl ${baseUrl} nor relativeBaseUrl ${relativeBaseUrl}`);
		}

		return rtrimSlashes(ltrimSlashes(output));
	}

	statsFromResources_(resources) {
		const relativeBaseUrl = this.api().relativeBaseUrl();
		const baseUrl = this.api().baseUrl();
		const output = [];
		for (let i = 0; i < resources.length; i++) {
			const resource = resources[i];
			const href = this.api().stringFromJson(resource, ['d:href', 0]);
			const path = this.hrefToRelativePath_(href, baseUrl, relativeBaseUrl);
			// if (href.indexOf(relativeBaseUrl) !== 0) throw new Error('Path "' + href + '" not inside base URL: ' + relativeBaseUrl);
			// const path = rtrimSlashes(ltrimSlashes(href.substr(relativeBaseUrl.length)));
			if (path === '') continue; // The list of resources includes the root dir too, which we don't want
			const stat = this.statFromResource_(resources[i], path);
			output.push(stat);

View on GitHub (pinned to 2654b33620)

Solutions

  1. Make the configured WebDAV base URL exactly match the href prefix the server returns (same scheme, host, port, path).
  2. If behind a reverse proxy, fix the proxy to pass through the original host/path or adjust the base URL to the proxy-visible value.
  3. Ensure scheme consistency (use https if the server redirects to https).
  4. Decode/encode percent-encoded characters in the base URL to match the server's encoding.

Example fix

// before
if (href.indexOf(baseUrl) === 0) { output = href.substr(baseUrl.length); }
else if (href.indexOf(relativeBaseUrl) === 0) { output = href.substr(relativeBaseUrl.length); }
else if (decodeURIComponent(href).indexOf(decodeURIComponent(relativeBaseUrl)) === 0) { output = decodeURIComponent(href).substring(decodeURIComponent(relativeBaseUrl).length); }
else { throw new Error(`href ${href} not in baseUrl ${baseUrl} nor relativeBaseUrl ${relativeBaseUrl}`); }
// after - also tolerate scheme/port differences via URL parsing
const stripBase = (h, b) => { try { const H = new URL(h), B = new URL(b); if (H.pathname.indexOf(B.pathname) === 0) return H.pathname.slice(B.pathname.length); } catch {} return null; };
output = stripBase(href, baseUrl) ?? stripBase(href, relativeBaseUrl);
if (output === null) throw new Error(`href ${href} not under base ${baseUrl}`);
Defensive patterns

Strategy: validation

Validate before calling

// Ensure the configured base URL matches the server's href prefix.
function baseUrlMatches(base: string, sampleHref: string): boolean {
  try {
    const B = new URL(base), H = new URL(sampleHref);
    return H.pathname.indexOf(B.pathname) === 0;
  } catch { return sampleHref.indexOf(base) === 0; }
}
if (!baseUrlMatches(configuredBaseUrl, observedHref)) {
  throw new Error('Base URL does not match server href prefix; fix sync config.');
}

Type guard

function hrefBelongsToBase(href: string, baseUrl: string, relativeBaseUrl: string): boolean {
  return href.indexOf(baseUrl) === 0 || href.indexOf(relativeBaseUrl) === 0
    || decodeURIComponent(href).indexOf(decodeURIComponent(relativeBaseUrl)) === 0;
}

Try / catch

try {
  return await driver.list(path);
} catch (e) {
  if (/not in baseUrl/.test(e.message)) {
    throw new Error('Sync stopped: server hrefs do not match the configured WebDAV base URL. Re-link with the correct URL.');
  }
  throw e;
}

Prevention

When it happens

Trigger: list() or stat() returns a d:href that points outside the configured WebDAV base — e.g. the server rewrites hrefs to an absolute URL with a different host/path, returns a redirect target, or the configured base URL doesn't match the actual server URL.

Common situations: Base URL configured with http while server redirects to https (or vice-versa); behind a reverse proxy that rewrites the host/port; Nextcloud/Seafile returning hrefs with a different path prefix than the configured root; trailing-slash mismatch between config and server; CDN/front-end altering URLs.

Related errors


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