koala73/worldmonitor · error · ConvexError

Invalid returnUrl: must be a valid absolute URL

Error message

Invalid returnUrl: must be a valid absolute URL

What it means

`_createCheckoutSession` parses `args.returnUrl` with `new URL(...)`. A value that cannot be parsed as an absolute URL (e.g., a bare path like `/dashboard`) throws and is wrapped in this ConvexError. This is the first of two returnUrl validations; the origin allowlist check follows.

Source

Thrown at convex/payments/checkout.ts:223

    const msg = err instanceof Error ? err.message : String(err);
    console.error(`[checkout] pending-payment guard query failed (failing open): ${msg}`);
    return null;
  }
}

async function _createCheckoutSession(
  args: CheckoutArgs,
  user: UserInfo,
) {
  // Validate returnUrl to prevent open-redirect attacks.
  const siteUrl = process.env.SITE_URL ?? "https://worldmonitor.app";
  let returnUrl = siteUrl;
  if (args.returnUrl) {
    let parsedReturnUrl: URL;
    try {
      parsedReturnUrl = new URL(args.returnUrl);
    } catch {
      throw new ConvexError("Invalid returnUrl: must be a valid absolute URL");
    }

    if (!isTrustedReturnUrlOrigin(parsedReturnUrl.origin, new URL(siteUrl).origin)) {
      throw new ConvexError(
        "Invalid returnUrl: must use a trusted worldmonitor.app origin",
      );
    }
    returnUrl = parsedReturnUrl.toString();
  }

  // Build metadata: HMAC-signed userId for the webhook identity bridge.
  const metadata: Record<string, string> = {};
  metadata.wm_user_id = user.userId;
  metadata.wm_user_id_sig = await signUserId(user.userId);
  const anonymousClaimToken = ANON_ID_V4_REGEX.test(user.userId)
    ? await signAnonClaimToken(user.userId)
    : null;
  if (anonymousClaimToken) {

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Pass a full absolute URL including the scheme, e.g. `https://app.worldmonitor.app/dashboard`
  2. Omit `returnUrl` entirely to fall back to the default `SITE_URL` (https://worldmonitor.app)

Example fix

// before
createCheckout({ productId, returnUrl: "/dashboard" })
// after
createCheckout({ productId, returnUrl: "https://app.worldmonitor.app/dashboard" })
Defensive patterns

Strategy: validation

Validate before calling

// Validate returnUrl is an absolute URL before calling createCheckout.
function validAbsoluteUrl(u: string | undefined): boolean {
  if (!u) return true; // undefined falls back to SITE_URL
  try { new URL(u); return true; } catch { return false; }
}
if (!validAbsoluteUrl(args.returnUrl)) { /* block submission */ }

Type guard

function isAbsoluteUrl(value: string | undefined): value is string {
  if (!value) return false;
  try { new URL(value); return true; } catch { return false; }
}

Prevention

When it happens

Trigger: Passing `returnUrl` as a relative path (`/dashboard`), a scheme-less string (`worldmonitor.app/dashboard`), or any value the URL constructor rejects.

Common situations: Frontend passes a route path instead of a full URL; misconfigured env producing a partial string; client assumes the server will prefix the scheme.

Related errors


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/c27d60057fd785d1. Report an issue: GitHub.