{"record":{"id":"5356418f487fc187","repo":"DIYgod/RSSHub","slug":"douban-details-deta","errorCode":null,"errorMessage":"Douban 返回数据结构异常，可能触发反爬或限频。${details ? `上游信息：${details}` : ''}","messagePattern":"Douban 返回数据结构异常，可能触发反爬或限频。(.+?)` : ''\\}","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"lib/routes/douban/tv/coming.ts","lineNumber":188,"sourceCode":"                    url: apiUrl,\n                    searchParams,\n                    headers: {\n                        Accept: 'application/json',\n                        'User-Agent': apiClientUa,\n                    },\n                });\n                return response.data as ComingSoonResponse;\n            } catch (error) {\n                throw buildFetchError(error);\n            }\n        },\n        config.cache.routeExpire,\n        false\n    )) as ComingSoonResponse;\n\n    if (!Array.isArray(data.subjects)) {\n        const details = data.msg || data.message || data.reason;\n        throw new Error(`Douban 返回数据结构异常，可能触发反爬或限频。${details ? `上游信息：${details}` : ''}`);\n    }\n    if (data.subjects.length === 0) {\n        throw new Error('Douban 返回空数据，可能触发反爬或限频。请稍后重试。');\n    }\n\n    const subscriptionCount = data.count ?? 0;\n    const total = data.total ?? 0;\n    const sortedSubjects = data.subjects.toSorted((a, b) => {\n        if (sortBy === 'time') {\n            const timeDiff = getSortTimestamp(a.pubdate) - getSortTimestamp(b.pubdate);\n            if (timeDiff !== 0) {\n                return timeDiff;\n            }\n            return getWishCount(b.wish_count) - getWishCount(a.wish_count);\n        }\n        const wishDiff = getWishCount(b.wish_count) - getWishCount(a.wish_count);\n        if (wishDiff !== 0) {\n            return wishDiff;","sourceCodeStart":170,"sourceCodeEnd":206,"githubUrl":"https://github.com/DIYgod/RSSHub/blob/bed535e0879dc71c5aff6f1e7bd1ac21ede40115/lib/routes/douban/tv/coming.ts#L170-L206","documentation":"Thrown by the Douban TV coming-soon route when the API response's `subjects` field is not an array. The route calls Douban's Frodo API (`frodo.douban.com/api/v2/tv/coming_soon`) with HMAC-signed requests. When Douban's anti-crawler or rate-limiter kicks in, the API returns an error object (with `msg`/`message`/`reason` fields) instead of the expected `subjects` array. The error message appends any upstream detail from those fields.","triggerScenarios":"Douban's Frodo API returns a rate-limit or anti-crawl error (e.g. {msg: 'rate limited', subjects: undefined}); the API key or signing algorithm has been deprecated/changed by Douban; the API endpoint structure has changed; the server IP is temporarily blocked.","commonSituations":"High request frequency triggers Douban rate limiting; the hardcoded apiKey/apiSecret has been revoked by Douban; RSSHub instance runs from a datacenter IP range that Douban blocks.","solutions":["Wait and retry after a cooldown period — the route uses cache so the first successful response serves subsequent requests.","If the apiKey/apiSecret are stale, check for updates in the RSSHub repository.","Route through a residential proxy or run from a non-datacenter IP if IP-level blocking is suspected.","Inspect the upstream detail in the error message for the specific Douban error code/reason."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Pre-flight check: verify the API key is still valid by making a test request\nasync function checkDoubanApiHealth(): Promise<boolean> {\n    try {\n        const resp = await fetch(`${apiUrl}?count=1&apiKey=${apiKey}`, {\n            headers: { 'User-Agent': apiClientUa }\n        });\n        const data = await resp.json();\n        return Array.isArray(data.subjects);\n    } catch {\n        return false;\n    }\n}","typeGuard":"function isComingSoonResponse(data: unknown): data is { subjects: unknown[] } {\n    return typeof data === 'object' && data !== null && Array.isArray((data as any).subjects);\n}","tryCatchPattern":"try {\n    const feed = await fetch(`${rsshubUrl}/douban/tv/coming`);\n} catch (e) {\n    if (e.message.includes('反爬或限频')) {\n        // Wait and retry — Douban rate limits are temporary\n        await new Promise(r => setTimeout(r, 60000));\n        return fetch(`${rsshubUrl}/douban/tv/coming`);\n    }\n    throw e;\n}","preventionTips":["Poll the Douban route infrequently to avoid triggering rate limits.","Rely on RSSHub's built-in cache rather than polling at high frequency.","If self-hosting, consider routing through a residential proxy.","Monitor the upstream detail in the error message for specific Douban error reasons."],"tags":["douban","anti-crawl","rate-limiting","api","response-validation"],"backgroundTag":null,"analyzedSha":"bed535e0879dc71c5aff6f1e7bd1ac21ede40115","analyzedAt":"2026-08-12T19:29:35.364Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}