{"record":{"id":"2544b432f57f97cd","repo":"jackwener/OpenCLI","slug":"douyin-hashtag-search-api-returned-suggestions-bu","errorCode":null,"errorMessage":"douyin hashtag search: API returned suggestions but none had a stable shape","messagePattern":"douyin hashtag search: API returned suggestions but none had a stable shape","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/douyin/hashtag.js","lineNumber":77,"sourceCode":"        const action = kwargs.action;\n        if (action === 'search') {\n            const keyword = String(kwargs.keyword ?? '').trim();\n            // challenge/search answers 200 with an empty body; the creator\n            // studio composer reads suggestions from this endpoint instead,\n            // which ignores count and returns a fixed-size list (#2205).\n            const url = `https://creator.douyin.com/aweme/v1/search/challengesug/?keyword=${encodeURIComponent(keyword)}&source=challenge_create&aid=2906`;\n            const res = await browserFetch(page, 'GET', url);\n            const list = requireListField(res, 'sug_list', 'search');\n            const rows = list.flatMap(c => {\n                if (!isPlainObject(c) || typeof c.cha_name !== 'string' || !c.cha_name) return [];\n                return [{\n                    name: c.cha_name,\n                    id: c.cid ?? '',\n                    view_count: c.view_count ?? 0,\n                }];\n            });\n            if (list.length > 0 && rows.length === 0) {\n                throw new CommandExecutionError('douyin hashtag search: API returned suggestions but none had a stable shape');\n            }\n            return rows.slice(0, kwargs.limit);\n        }\n        if (action === 'suggest') {\n            const cover = String(kwargs.cover ?? '').trim();\n            const url = `https://creator.douyin.com/web/api/media/hashtag/rec/?cover_uri=${encodeURIComponent(cover)}&aid=1128`;\n            const res = await browserFetch(page, 'GET', url);\n            const list = requireListField(res, 'hashtag_list', 'suggest');\n            return list.map(h => ({ name: h?.name ?? '', id: h?.id ?? '', view_count: h?.view_count ?? 0 }));\n        }\n        if (action === 'hot') {\n            const kw = String(kwargs.keyword ?? '').trim();\n            const url = `https://creator.douyin.com/aweme/v1/hotspot/recommend/?${kw ? `keyword=${encodeURIComponent(kw)}&` : ''}aid=1128`;\n            const res = await browserFetch(page, 'GET', url);\n            if (!isPlainObject(res)) {\n                throw new CommandExecutionError('douyin hashtag hot: API returned malformed payload');\n            }\n            const hotspotList = res.hotspot_list;","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/douyin/hashtag.js#L59-L95","documentation":"Thrown by the douyin hashtag CLI 'search' action when the creator.douyin.com suggestion API returned data, but none of the suggestion entries could be normalized into the expected row shape ({name, id, view_count}). The library treats an all-unusable suggestion list as a failure rather than silently returning empty results.","triggerScenarios":"The suggestion endpoint responds with JSON where the candidate array exists but every entry lacks recognizable fields (e.g. cha_name/cid), or rows end up empty while the raw list is non-empty, so `list.length > 0 && rows.length === 0` becomes true at clis/douyin/hashtag.js:77.","commonSituations":"Douyin changed the suggestion response schema (renamed cha_name/cid); the endpoint returned an HTML login page or captcha JSON instead of suggestions; the API is behind an A/B test returning a new shape.","solutions":["Re-run the command; transient payloads (captcha/anti-bot interstitials) often resolve on retry with a warm logged-in session.","Verify the browser session is logged in to creator.douyin.com (cookies fresh, not expired).","Check for a library update — Douyin frequently changes the response schema; a newer version may map the new fields.","Inspect the raw API response manually (same URL in the logged-in browser) to confirm the schema and file an issue if it changed.","Fall back to the 'hot' action or a different keyword if search suggestions are unavailable for the term."],"exampleFix":"// before: assuming every suggestion has usable fields\nconst rows = list.map(c => [{ name: c.cha_name, id: c.cid ?? '', view_count: c.view_count ?? 0 }]);\n// after: log the unexpected shape before throwing, to aid diagnosis\nif (list.length > 0 && rows.length === 0) {\n    console.error('raw suggestions:', JSON.stringify(list).slice(0, 500));\n    throw new CommandExecutionError('douyin hashtag search: API returned suggestions but none had a stable shape');\n}","handlingStrategy":"type-guard","validationCode":"// filter suggestions to those with a usable shape before calling\nconst usable = raw.filter(c => c && typeof c.cha_name === 'string' && c.cid != null);\nif (usable.length === 0) throw new Error('no well-formed douyin hashtag suggestions for this keyword');","typeGuard":"function isShapedSuggestion(c) {\n  return c != null && typeof c === 'object' && typeof c.cha_name === 'string' && (typeof c.cid === 'string' || typeof c.cid === 'number');\n}","tryCatchPattern":"try {\n  const rows = await hashtag({ action: 'search', keyword: kw, limit: 10 });\n} catch (e) {\n  if (String(e.message).includes('none had a stable shape')) {\n    // fall back to hot list or surface a schema-change warning\n    console.warn('douyin suggestion schema changed; falling back');\n  } else throw e;\n}","preventionTips":["Keep the library updated — Douyin changes response schemas often.","Check login state before calling search.","Log raw suggestion payloads when the error occurs to detect schema drift early.","Retry once before treating it as a hard failure."],"tags":["douyin","api-schema","response-parsing"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}