DIYgod/RSSHub · error · ConfigNotFoundError

无权访问 id 为 ${catalogId} 的 List(可能是未设置 Cookie 或 Cookie 已过期)

Error message

无权访问 id 为 ${catalogId} 的 List(可能是未设置 Cookie 或 Cookie 已过期)

What it means

ConfigNotFoundError thrown by the Medium 'list' route when the getUserCatalogMainContentQuery response's __typename is 'Forbidden'. Medium's API explicitly denied access to the requested catalog/list — almost always because no cookie or an expired cookie was sent for that user.

Source

Thrown at lib/routes/medium/list.ts:42

    handler,
    description: `The List ID is the last part of the URL after \`-\`, for example, the username in <https://medium.com/@imsingee/list/collection-7e67004f23f9> is \`imsingee\`, and the ID is \`7e67004f23f9\`.

::: warning
To access private lists, only self-hosting is supported.
:::`,
};

async function handler(ctx) {
    const user = ctx.req.param('user');
    const catalogId = ctx.req.param('catalogId');

    const cookie = config.medium.cookies[user];

    const catalog = await getUserCatalogMainContentQuery(user, catalogId, cookie);
    ctx.set('json', catalog);

    if (catalog && catalog.__typename === 'Forbidden') {
        throw new ConfigNotFoundError(`无权访问 id 为 ${catalogId} 的 List(可能是未设置 Cookie 或 Cookie 已过期)`);
    }
    if (!catalog || !catalog.itemsConnection) {
        throw new InvalidParameterError(`id 为 ${catalogId} 的 List 不存在`);
    }

    const name = catalog.name;
    const urls = catalog.itemsConnection.items.map((item) => item.entity.mediumUrl);

    const parsedArticles = await Promise.all(urls.map((url) => parseArticle(ctx, url)));

    return {
        title: `List: ${name}`,
        link: `https://medium.com/@${user}/list/${catalogId}`,
        item: parsedArticles,
    };
}

View on GitHub (pinned to bed535e087)

Solutions

  1. Set config.medium.cookies[user] (MEDIUM_COOKIES env) with a valid session cookie from the account that owns or follows the list.
  2. Re-login and refresh the cookie if expired.
  3. Confirm the list is accessible by the logged-in account in a browser.
  4. Verify both :user and :catalogId path parameters are correct.

Example fix

// before
// config.medium.cookies[user] unset -> cookie=undefined -> Forbidden

// after
MEDIUM_COOKIES=johndoe=sid%3A1%3A...
Defensive patterns

Strategy: validation

Validate before calling

const cookie = config.medium.cookies?.[user];
if (!cookie) {
    throw new ConfigNotFoundError(`Set Medium cookie for ${user} to access private lists`);
}

Type guard

function hasMediumCookie(user: string): boolean {
    return Boolean(config.medium.cookies?.[user]);
}

Try / catch

const catalog = await getUserCatalogMainContentQuery(user, catalogId, cookie);
if (catalog?.__typename === 'Forbidden') {
    throw new ConfigNotFoundError('Access denied — refresh the Medium cookie');
}

Prevention

When it happens

Trigger: getUserCatalogMainContentQuery(user, catalogId, cookie) resolves to an object whose __typename === 'Forbidden'. This is Medium's access-control rejection for private or member-gated lists.

Common situations: config.medium.cookies[user] missing (cookie is undefined, sent as undefined); cookie expired so Medium treats the request as anonymous; the list is private to a different account; list was deleted/locked.

Related errors


AI-assisted analysis of DIYgod/RSSHub@bed535e087 (2026-08-12). Data as JSON: /api/errors/4e040ad55020b0c8. Report an issue: GitHub.