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
- 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.
- 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.
- If behind a reverse proxy, ensure it preserves/forwards the original Host header so ctx.URL.origin reflects the real request origin.
- 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.
- 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
- Keep APP_BASE_URL (and USER_CONTENT_BASE_URL) in sync with the exact host/port clients use to reach the server, including scheme and port.
- Configure reverse proxies to pass through the original Host header (e.g. proxy_set_header Host $host).
- In dev, use one canonical origin (localhost:PORT) for both the server config and all clients instead of mixing localhost, 127.0.0.1, and LAN IPs.
- After domain migrations, update the base URL config before pointing clients at the new domain.
- Log ctx.URL.origin alongside the configured base URL when this error occurs to spot mismatches immediately.
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
- No text editor is defined. Please set it using `config edito
- Cannot initialise synchroniser.
- Note not published: %s
- Whisper config is not an object
- ${errorPrefix}: Field "prompts" is not an object
AI-assisted analysis of laurent22/joplin@683240968b (2026-08-28).
Data as JSON: /api/errors/83d4893a587c9d67.
Report an issue: GitHub.