koala73/worldmonitor · error · ProMcpIssueFailed

invalid-user-id

invalid-user-id

Error message

Invalid userId for Pro MCP token issue

What it means

Convex answered HTTP 400 (INVALID_USER_ID) for the internal token issue action: the userId in the request body was empty, missing, or not a usable user reference. issueProMcpTokenForUser maps this to ProMcpIssueFailed kind='invalid-user-id'. It is a caller bug, deterministic for the same input — retrying with the same userId will fail identically.

Source

Thrown at server/_shared/pro-mcp-token.ts:224

      'network',
      `Convex issue request failed: ${err instanceof Error ? err.message : String(err)}`,
    );
  }

  if (resp.ok) {
    const data = (await resp.json().catch(() => null)) as ProMcpIssueResult | null;
    if (!data || typeof data.tokenId !== 'string' || !data.tokenId) {
      throw new ProMcpIssueFailed('network', 'Convex issue response missing tokenId', resp.status);
    }
    return { tokenId: data.tokenId };
  }

  // Map Convex error responses (see convex/http.ts /api/internal-issue-pro-mcp-token).
  if (resp.status === 403) {
    throw new ProMcpIssueFailed('pro-required', 'Pro entitlement required to issue MCP token', 403);
  }
  if (resp.status === 400) {
    throw new ProMcpIssueFailed('invalid-user-id', 'Invalid userId for Pro MCP token issue', 400);
  }
  // 401 (shared-secret mismatch) and 5xx and any other status → network/transient.
  throw new ProMcpIssueFailed(
    'network',
    `Convex issue returned HTTP ${resp.status}`,
    resp.status,
  );
}

/**
 * Validate a Pro MCP token by tokenId — discriminated-union variant.
 *
 * Returns `{ok:'valid', userId}` if the row exists and is not revoked.
 * Returns `{ok:'revoked'}` if Convex authoritatively returned null
 * (row missing, revoked, or malformed-id). Returns `{ok:'transient'}` on
 * Convex 5xx / network error / timeout / non-JSON — caller can decide
 * whether to fail-closed (per-request validate) or preserve the refresh
 * token (refresh-grant path) instead of consuming it.

View on GitHub (pinned to eeab0a219f)

Solutions

  1. Log and inspect the exact userId being sent in the request body at the call site
  2. Guard the call: only invoke issueProMcpTokenForUser after confirming userId is a non-empty string
  3. Check how the caller derives userId (Clerk subject vs Convex user table ID) and align it with what convex/http.ts validates

Example fix

// before — session hiccup silently produces an empty userId
const userId = session?.user?.id ?? '';
await issueProMcpTokenForUser(userId);

// after — fail fast before hitting Convex
const userId = session?.user?.id;
if (!userId) return new Response('unauthenticated', { status: 401 });
await issueProMcpTokenForUser(userId);
Defensive patterns

Strategy: validation

Validate before calling

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

if (!isValidIssueUserId(userId)) return new Response('missing user', { status: 401 });

Type guard

function isInvalidUserId(e: unknown): e is ProMcpIssueFailed {
  return e instanceof ProMcpIssueFailed && e.kind === 'invalid-user-id';
}

Try / catch

try {
  await issueProMcpTokenForUser(userId);
} catch (err) {
  if (isInvalidUserId(err)) return new Response('invalid user id', { status: 400 }); // fix input, do not retry
  throw err;
}

Prevention

When it happens

Trigger: issueProMcpTokenForUser('') or issueProMcpTokenForUser(undefined as any) — e.g. the Clerk session resolved without a subject; the caller passed an internal/clerk-prefixed ID where Convex expects its own user table reference; JSON.stringify body dropped userId because the variable was never assigned.

Common situations: OAuth handler reads userId from a session that expired mid-flow, yielding an empty string; a refactor changed the userId source and TypeScript did not catch the undefined because of an any cast; the Convex user row was deleted between grant and issue.

Related errors


AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21). Data as JSON: /api/errors/35bff87d8ac94fb3. Report an issue: GitHub.