{"record":{"id":"4e040ad55020b0c8","repo":"DIYgod/RSSHub","slug":"id-catalogid-list-cookie-cook","errorCode":null,"errorMessage":"无权访问 id 为 ${catalogId} 的 List（可能是未设置 Cookie 或 Cookie 已过期）","messagePattern":"无权访问 id 为 (.+?) 的 List（可能是未设置 Cookie 或 Cookie 已过期）","errorType":"exception","errorClass":"ConfigNotFoundError","httpStatus":null,"severity":"error","filePath":"lib/routes/medium/list.ts","lineNumber":42,"sourceCode":"    handler,\n    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\\`.\n\n::: warning\nTo access private lists, only self-hosting is supported.\n:::`,\n};\n\nasync function handler(ctx) {\n    const user = ctx.req.param('user');\n    const catalogId = ctx.req.param('catalogId');\n\n    const cookie = config.medium.cookies[user];\n\n    const catalog = await getUserCatalogMainContentQuery(user, catalogId, cookie);\n    ctx.set('json', catalog);\n\n    if (catalog && catalog.__typename === 'Forbidden') {\n        throw new ConfigNotFoundError(`无权访问 id 为 ${catalogId} 的 List（可能是未设置 Cookie 或 Cookie 已过期）`);\n    }\n    if (!catalog || !catalog.itemsConnection) {\n        throw new InvalidParameterError(`id 为 ${catalogId} 的 List 不存在`);\n    }\n\n    const name = catalog.name;\n    const urls = catalog.itemsConnection.items.map((item) => item.entity.mediumUrl);\n\n    const parsedArticles = await Promise.all(urls.map((url) => parseArticle(ctx, url)));\n\n    return {\n        title: `List: ${name}`,\n        link: `https://medium.com/@${user}/list/${catalogId}`,\n        item: parsedArticles,\n    };\n}\n","sourceCodeStart":24,"sourceCodeEnd":59,"githubUrl":"https://github.com/DIYgod/RSSHub/blob/bed535e0879dc71c5aff6f1e7bd1ac21ede40115/lib/routes/medium/list.ts#L24-L59","documentation":"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.","triggerScenarios":"getUserCatalogMainContentQuery(user, catalogId, cookie) resolves to an object whose __typename === 'Forbidden'. This is Medium's access-control rejection for private or member-gated lists.","commonSituations":"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.","solutions":["Set config.medium.cookies[user] (MEDIUM_COOKIES env) with a valid session cookie from the account that owns or follows the list.","Re-login and refresh the cookie if expired.","Confirm the list is accessible by the logged-in account in a browser.","Verify both :user and :catalogId path parameters are correct."],"exampleFix":"// before\n// config.medium.cookies[user] unset -> cookie=undefined -> Forbidden\n\n// after\nMEDIUM_COOKIES=johndoe=sid%3A1%3A...","handlingStrategy":"validation","validationCode":"const cookie = config.medium.cookies?.[user];\nif (!cookie) {\n    throw new ConfigNotFoundError(`Set Medium cookie for ${user} to access private lists`);\n}","typeGuard":"function hasMediumCookie(user: string): boolean {\n    return Boolean(config.medium.cookies?.[user]);\n}","tryCatchPattern":"const catalog = await getUserCatalogMainContentQuery(user, catalogId, cookie);\nif (catalog?.__typename === 'Forbidden') {\n    throw new ConfigNotFoundError('Access denied — refresh the Medium cookie');\n}","preventionTips":["Always configure a cookie for list routes.","Confirm list visibility for the logged-in account.","Differentiate Forbidden (cookie) from missing (catalogId) in operator alerts."],"tags":["medium","auth","cookie","forbidden"],"backgroundTag":null,"analyzedSha":"bed535e0879dc71c5aff6f1e7bd1ac21ede40115","analyzedAt":"2026-08-12T19:29:35.364Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}