laurent22/joplin · error · ErrorNotFound

InvalidOrigin

InvalidOrigin

Error message

Invalid origin: ${ctx.URL.origin}

What it means

The Joplin server throws this error (as an ErrorNotFound with code InvalidOrigin) when the Origin of the incoming request does not match the base URL configured for the endpoint's route type. It is an anti-DNS-rebinding / host-spoofing check: the request's host must equal the configured app base URL host (for UserContent routes, either the exact usercontent host or any single-label subdomain of it). When the hosts differ, the request is rejected as invalid before session or permission checks run.

Source

Thrown at packages/server/src/utils/routeUtils.ts:217

}

function disabledAccountCheck(route: MatchedRoute, user: User) {
	if (!user || user.enabled) return;

	if (route.subPath.schema.startsWith('api/')) throw new ErrorForbidden(`This account is disabled. Please login to ${config().baseUrl} for more information.`);
}

interface ExecRequestResult {
	response: unknown;
	path: SubPath;
}

export async function execRequest(routes: Routers, ctx: AppContext): Promise<ExecRequestResult> {
	const match = findMatchingRoute(ctx.path, routes);
	if (!match) throw new ErrorNotFound();

	const endPoint = match.route.findEndPoint(ctx.request.method as HttpMethod, match.subPath.schema);
	if (ctx.URL && !isValidOrigin(ctx.URL.origin, baseUrl(endPoint.type), endPoint.type)) throw new ErrorNotFound(`Invalid origin: ${ctx.URL.origin}`, ErrorCode.InvalidOrigin);

	const isPublicRoute = match.route.isPublic(match.subPath.schema, ctx.request.method as HttpMethod);

	// This is a generic catch-all for all private end points - if we
	// couldn't get a valid session, we exit now. Individual end points
	// might have additional permission checks depending on the action.
	if (!isPublicRoute && !ctx.joplin.owner) {
		if (contextSessionId(ctx, false)) {
			// If we have a session but not a user it means the session was
			// invalid or has expired, so display a special message, since this
			// is also going to be displayed on the website.
			throw new ErrorForbidden('Your session has expired. Please login again.');
		} else {
			throw new ErrorForbidden();
		}
	}

	await csrfCheck(ctx, isPublicRoute);

View on GitHub (pinned to 683240968b)

Solutions

  1. Verify how you are reaching the server (scheme, host, port) and make it match APP_BASE_URL (or the user-content base URL) exactly, including port.
  2. Check the server config: APP_BASE_URL (and USER_CONTENT_BASE_URL if set) must be the same host clients actually use; update it after domain/proxy changes.
  3. If behind a reverse proxy, ensure it preserves/forwards the original Host header so ctx.URL.origin reflects the real request origin.
  4. If you intentionally need multiple hosts (e.g. localhost in dev), point APP_BASE_URL at the host you use, or add proxy/redirect rules so all access goes through the canonical base URL.
  5. For API clients, confirm your client is not injecting an Origin header for a different origin (some SDKs and redirects do).

Example fix

// before: server env
APP_BASE_URL=https://notes.example.com
// client calls http://localhost:22300/api/ping -> InvalidOrigin

// after: access via the configured base URL
curl https://notes.example.com/api/ping
// or, for local dev, set the env to match:
APP_BASE_URL=http://localhost:22300
Defensive patterns

Strategy: validation

Validate before calling

// Before calling the API, check that the URL you use matches the server's configured base URL
import { URL } from 'url';

function assertMatchingOrigin(requestUrl: string, configuredBaseUrl: string): boolean {
	try {
		return new URL(requestUrl).host === new URL(configuredBaseUrl).host;
	} catch {
		return false;
	}
}

// Usage:
// if (!assertMatchingOrigin('http://localhost:22300/api/ping', process.env.APP_BASE_URL)) throw new Error('Use the configured APP_BASE_URL host');

Type guard

null

Try / catch

// If you cannot pre-validate, catch and inspect the code:
try {
	await apiClient.ping();
} catch (error) {
	if (error && (error as any).code === 'InvalidOrigin') {
		// host you used differs from the server's configured base URL
		throw new Error(`Request host does not match the server's APP_BASE_URL: ${error.message}`);
	}
	throw error;
}

Prevention

When it happens

Trigger: Calling any Joplin server route (e.g. GET /api/ping or a /share user-content URL) from a host that differs from config().APP_BASE_URL (or baseUrl(RouteType.UserContent) for user-content routes). Concretely: accessing the server via http://localhost:22300 while APP_BASE_URL is set to https://notes.example.com; accessing a user-content route via a bare domain when the URL uses a per-user subdomain (userid.example.com vs example.com); or an HTTP client (browser fetch/POST) sending an Origin header for a different port than the configured base URL.

Common situations: Misconfigured APP_BASE_URL in the server env (points at a different domain/port than how clients actually reach the server); accessing the server through localhost, 127.0.0.1, a LAN IP, or a reverse proxy that rewrites Host while baseUrl is the public domain; dev environments where the base URL was set for production; proxy not forwarding the original Host header; changing domains without updating the base URL config.

Related errors


AI-assisted analysis of laurent22/joplin@683240968b (2026-08-28). Data as JSON: /api/errors/83d4893a587c9d67. Report an issue: GitHub.