TryGhost/Ghost · error · Error

errors.invalidUsername

Error message

errors.invalidUsername

What it means

This is the generic handleToUrl guard inside createPlatformValidator. It throws errors.invalidUsername (the message configured per-platform) when handleToUrl is called with an empty, null, or whitespace-only handle. handleToUrl is meant to convert a stored handle to a canonical URL and has no sensible output for an empty handle, so it fails fast rather than returning a malformed URL.

Source

Thrown at apps/admin/src/settings/app/utils/social-urls/platform-validator.ts:280

        if (!validator.isURL(url)) {
            throw new Error(errors.invalidUrl);
        }
        return url;
    };

    const validate = (input: string) => {
        if (!input) {
            return '';
        }
        const {region, pathType, rawUsername, atPrefixConsumed} = extractParts(input.trim());
        const username = formatUsername(rawUsername, atPrefixConsumed);
        checkUsername(pathType, username);
        return buildUrl(pathType, username, region);
    };

    const handleToUrl = (handle: string) => {
        if (!handle) {
            throw new Error(errors.invalidUsername);
        }
        const {pathType, rawUsername, atPrefixConsumed} = parseHandle(handle.trim());
        const username = formatUsername(rawUsername, atPrefixConsumed);
        checkUsername(pathType, username);
        return buildUrl(pathType, username);
    };

    const urlToHandle = (url: string) => {
        if (!url || !isUrlInput(url.trim())) {
            return null;
        }
        try {
            const {pathType, rawUsername, atPrefixConsumed} = extractParts(url.trim());
            const username = formatUsername(rawUsername, atPrefixConsumed);
            checkUsername(pathType, username);
            // the regional subdomain (uk.linkedin.com) is intentionally dropped:
            // stored handles are region-less
            return `${pathType.storagePrefix}${username}`;

View on GitHub (pinned to 47d8b0e2ad)

Solutions

  1. Guard the call site: check the handle is a non-empty string before calling handleToUrl.
  2. Use validate instead of handleToUrl when the input may be empty — validate returns '' for empty input rather than throwing.
  3. If rendering, treat empty handles as 'no profile set' and skip the URL generation entirely.
  4. Coerce with a default or early-return in the caller: if (!handle) return null;

Example fix

// before (throws when handle is empty)
const url = blueskyHandleToUrl(member.bluesky_handle);
// after
const url = member.bluesky_handle ? blueskyHandleToUrl(member.bluesky_handle) : null;
Defensive patterns

Strategy: validation

Validate before calling

function safeHandleToUrl(handleToUrl: (h: string) => string, handle: string | null | undefined): string | null {
  if (!handle || !handle.trim()) {
    return null;
  }
  return handleToUrl(handle);
}

Type guard

function isNonEmptyHandle(value: unknown): value is string {
  return typeof value === 'string' && value.trim().length > 0;
}

Try / catch

try {
  const url = blueskyHandleToUrl(handle);
} catch (e) {
  if (e instanceof Error && /not a valid/.test(e.message)) {
    // handle was empty or invalid — treat as no profile
    return null;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling any platform's handleToUrl export (e.g. blueskyHandleToUrl, threadsHandleToUrl, twitterHandleToUrl, linkedinHandleToUrl) with an empty string, undefined-coerced-to-string, or a handle that trims to empty. The check is `if (!handle)` which catches '', 0, null, undefined, NaN, and false.

Common situations: A settings form that loads a blank social profile field from the database and calls handleToUrl to render the canonical URL without first checking the field is populated. A migration or default-seeding script that passes undefined for a new user with no social profiles set.

Related errors


AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13). Data as JSON: /api/errors/1553f0dd5d123940. Report an issue: GitHub.