jackwener/OpenCLI · error · CliError

API_ERROR

API_ERROR

Error message

API_ERROR: ${data.errmsg ?? `WeRead API error ${data.errcode}`}

What it means

The WeRead CLI's fetchPrivateApi wraps every authenticated WeRead API call. When the HTTP response is OK but the JSON body carries a non-zero errcode, the adapter normalizes it into a CliError with code API_ERROR so callers get one consistent error type for upstream API rejections. The message is the server-provided errmsg, or a fallback naming the numeric errcode when the server omits one.

Solutions

  1. Log out and back in to weread.qq.com in Chrome to refresh the session cookies, then retry
  2. Print the raw body (data.errcode/errmsg) to see which upstream error code is being returned and consult WeRead's error semantics
  3. Re-check the arguments passed to the API call (bookId, path, query params) for typos or stale IDs
  4. Retry after a delay in case of transient WeRead-side failures
  5. If an errcode appears consistently, update the CLI to handle the new upstream error code

Example fix

// before
const data = await fetchPrivateApi('shelf/friendCommon', { user });
// after
try {
    const data = await fetchPrivateApi('shelf/friendCommon', { user });
} catch (e) {
    if (e.code === 'API_ERROR') console.error(`WeRead rejected the request: ${e.message} (re-login if AUTH_REQUIRED persists)`);
    throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// best pre-check: ensure login cookies exist before calling
if (!hasWeReadCookies()) throw new Error('Log in to weread.qq.com in Chrome first');

Type guard

function isApiError(e) { return e instanceof Error && e.code === 'API_ERROR'; }

Try / catch

try {
    const data = await fetchPrivateApi(path, params);
} catch (e) {
    if (e.code === 'API_ERROR') {
        // e.message is upstream errmsg or `WeRead API error <errcode>`; re-auth may fix it
    } else if (e.code === 'AUTH_REQUIRED') {
        // prompt login flow
    } else throw e;
}

Prevention

When it happens

Trigger: Any fetchPrivateApi call (via the data/result helpers) where resp.ok is true but the response body has data.errcode set to a non-zero value, e.g. an expired session cookie, invalid bookId, or a WeRead-side business rejection.

Common situations: Session cookies from Chrome have expired or been rotated; querying a bookId the account cannot access; WeRead changes an endpoint's error contract and starts returning errcode in a 200 response; transient WeRead service degradation.

Related errors


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

Appendix: source

Thrown at clis/weread/utils.js:177

    }
    catch (error) {
        throw new CliError('FETCH_ERROR', `Failed to fetch ${path}: ${error instanceof Error ? error.message : String(error)}`, 'WeRead API may be temporarily unavailable');
    }
    let data;
    try {
        data = await resp.json();
    }
    catch {
        throw new CliError('PARSE_ERROR', `Invalid JSON response for ${path}`, 'WeRead may have returned an HTML error page');
    }
    if (isAuthErrorResponse(resp, data)) {
        throw new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first');
    }
    if (!resp.ok) {
        throw new CliError('FETCH_ERROR', `HTTP ${resp.status} for ${path}`, 'WeRead API may be temporarily unavailable');
    }
    if (data?.errcode != null && data.errcode !== 0) {
        throw new CliError('API_ERROR', data.errmsg ?? `WeRead API error ${data.errcode}`);
    }
    return data;
}
function getUniqueRawBookIds(snapshot) {
    return Array.from(new Set(snapshot.rawBooks
        .map((book) => String(book?.bookId || '').trim())
        .filter(Boolean)));
}
/** Mirror of hasTrustedIndexes in buildShelfSnapshotPollScript — keep in sync */
function getTrustedIndexedBookIds(snapshot) {
    const rawBookIds = getUniqueRawBookIds(snapshot);
    if (rawBookIds.length === 0)
        return [];
    const rawBookIdSet = new Set(rawBookIds);
    const projectedIndexedBookIds = Array.from(new Set(snapshot.shelfIndexes
        .filter((entry) => Number.isFinite(entry?.idx))
        .sort((left, right) => Number(left?.idx ?? Number.MAX_SAFE_INTEGER) - Number(right?.idx ?? Number.MAX_SAFE_INTEGER))
        .map((entry) => String(entry?.bookId || '').trim())

View on GitHub (pinned to 49907e53dc)