jackwener/OpenCLI · error · ArgumentError

xiaohongshu/unfollow: profile URL must be /user/profile/<use

Error message

xiaohongshu/unfollow: profile URL must be /user/profile/<userId>

What it means

assertUserId accepts only canonical profile URLs whose path is exactly /user/profile/<userId> with an 8-32 alphanumeric ID. This ArgumentError is thrown when the URL is a valid https xiaohongshu.com URL but the path does not match that pattern, so no user ID can be extracted.

Source

Thrown at clis/xiaohongshu/unfollow.js:50

    }
    return inner;
}

function assertUserId(raw) {
    const input = String(raw ?? '').trim();
    if (/^https?:\/\//i.test(input)) {
        let parsed;
        try {
            parsed = new URL(input);
        } catch {
            throw new ArgumentError('xiaohongshu/unfollow: invalid profile URL');
        }
        if (parsed.protocol !== 'https:' || !isXiaohongshuHost(parsed.hostname)) {
            throw new ArgumentError('xiaohongshu/unfollow: profile URL must be an exact https://*.xiaohongshu.com URL');
        }
        const match = parsed.pathname.match(/^\/user\/profile\/([a-zA-Z0-9]{8,32})\/?$/);
        if (!match) {
            throw new ArgumentError('xiaohongshu/unfollow: profile URL must be /user/profile/<userId>');
        }
        return match[1];
    }
    const userId = normalizeXhsUserId(raw);
    if (!userId || !USER_ID_RE.test(userId)) {
        throw new ArgumentError(
            'xiaohongshu/unfollow: user-id must be a Xiaohongshu user ID (e.g. 5d8f88dc0000000001005d3a) or full profile URL',
        );
    }
    return userId;
}

/**
 * Click the 已关注 / 已互关 button on the profile (idempotent if not following).
 * Returns `{ ok, state }` where state is 'unfollow-clicked' | 'not-following' | 'failed'.
 */
function buildClickUnfollowScript() {
    return `

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Open the target user's page in a browser and copy the address while on their profile: it must look like https://www.xiaohongshu.com/user/profile/<userId>
  2. Extract the ID from the URL yourself and pass just the ID: xiaohongshu unfollow 5d8f88dc0000000001005d3a
  3. Remove extra path segments or trailing junk after the 8-32 char alphanumeric ID (a single trailing slash is allowed)
  4. Confirm you have a profile URL, not a note (/explore/), board (/board/), or search (/search_result/) URL

Example fix

// before
$ xiaohongshu unfollow 'https://www.xiaohongshu.com/explore/65f0a1b2000000001203e4c5'
// after
$ xiaohongshu unfollow 'https://www.xiaohongshu.com/user/profile/5d8f88dc0000000001005d3a'
Defensive patterns

Strategy: validation

Validate before calling

const m = new URL(input).pathname.match(/^\/user\/profile\/([a-zA-Z0-9]{8,32})\/?$/);
if (!m) throw new Error('Not a profile URL: ' + input);

Type guard

function isProfileUrl(input) {
  try { return /^\/user\/profile\/([a-zA-Z0-9]{8,32})\/?$/.test(new URL(String(input).trim()).pathname); }
  catch { return false; }
}

Try / catch

try { await cli.unfollow({ 'user-id': url }); } catch (e) { if (String(e.message).includes('/user/profile/<userId>')) { /* extract ID or get the correct profile URL */ } else throw e; }

Prevention

When it happens

Trigger: Passing 'https://www.xiaohongshu.com/user/profile/' (missing ID); 'https://www.xiaohongshu.com/explore/<noteId>' (a note URL, not a user profile); 'https://www.xiaohongshu.com/user/profile/<id>/follow' (extra path segments); an ID with non-alphanumeric characters or shorter/longer than 32 chars; a user page with a custom path like /user/profile/<id>?tab=... is fine, but /users/ or /profile/ variants fail.

Common situations: Grabbing the URL of a note/board/search page instead of the user's profile; copying a URL while the site was still redirecting (e.g. /discovery/item); URL-encoding the path; using a truncated URL from a log line.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/34e88de7e72f8b67. Report an issue: GitHub.