Skip to content

fix(route/twitter): dynamically resolve GraphQL query IDs and fix production CookieAgent#21544

Open
yuguorui wants to merge 2 commits intoDIYgod:masterfrom
yuguorui:fix/twitter-dynamic-gql-id
Open

fix(route/twitter): dynamically resolve GraphQL query IDs and fix production CookieAgent#21544
yuguorui wants to merge 2 commits intoDIYgod:masterfrom
yuguorui:fix/twitter-dynamic-gql-id

Conversation

@yuguorui
Copy link
Copy Markdown
Contributor

Involved Issue / 该 PR 相关 Issue

Close #18894

Example for the Proposed Route(s) / 路由地址示例

/twitter/home
/twitter/home_latest
/twitter/user/:id
/twitter/keyword/:keyword
/twitter/list/:id

New RSS Route Checklist / 新 RSS 路由检查表

  • New Route / 新的路由
  • Anti-bot or rate limit / 反爬/频率限制
    • If yes, do your code reflect this sign? / 如果有, 是否有对应的措施?
  • Date and time / 日期和时间
    • Parsed / 可以解析
    • Correct time zone / 时区正确
  • New package added / 添加了新的包
  • Puppeteer

Note / 说明

Two fixes for the Twitter web API routes:

1. Dynamically resolve GraphQL query IDs (gql-id-resolver.ts)

Twitter rotates GraphQL query IDs every 2-4 weeks, causing 404 errors on all Twitter routes. This adds a gql-id-resolver module that:

  • Fetches Twitter's homepage HTML and extracts JS bundle URLs
  • Downloads bundles and extracts queryId/operationName pairs via regex
  • Caches resolved IDs for 24 hours (Redis supported)
  • Falls back to hardcoded IDs if dynamic resolution fails
  • Hooks into api.init() so resolution happens before the first API call

2. Fix production build CookieAgent dispatcher loss (utils.ts)

In production builds, the Twitter API returns 404 with empty body because cookies are never sent. Root cause: both ofetch and the global wrappedFetch (request-rewriter) drop the dispatcher option when constructing new Request(input, init) — the standard Request class does not support dispatcher, so the CookieAgent is silently discarded. This means undici.fetch is called without the cookie jar.

Fix: use undici.fetch directly in twitterGot to preserve the CookieAgent dispatcher. All rate-limit and auth error handling logic is preserved.

Testing

  • /twitter/home and /twitter/home_latest work in dev mode
  • Intentionally invalid fallback IDs → dynamic resolution kicks in, routes still work
  • /twitter/home and /twitter/home_latest work in production build (was 404 before)
  • Passes oxlint and eslint pre-commit checks

…r JS bundles

Twitter rotates GraphQL query IDs every 2-4 weeks, causing 404 errors.
Instead of relying solely on hardcoded IDs, fetch and cache the latest
query IDs from Twitter's client-web JS bundles at runtime.

- Add gql-id-resolver module to extract queryId/operationName pairs
- Cache resolved IDs for 24 hours via RSSHub cache (Redis supported)
- Fall back to hardcoded IDs if dynamic resolution fails
- Update init() to trigger resolution before first API call

Signed-off-by: yuguorui <yuguorui@pku.edu.cn>
… dispatcher

Both ofetch and the global wrappedFetch (request-rewriter) drop the
dispatcher option when constructing a new Request object, which prevents
the CookieAgent from attaching cookies in production builds. Use
undici.fetch directly for Twitter API requests to preserve the dispatcher.

Signed-off-by: yuguorui <yuguorui@pku.edu.cn>
@github-actions github-actions bot added route auto: route no found Automated test failed due to route can not be found in PR description body labels Mar 29, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Please use actual values in routes section instead of path parameters.
请在 routes 部分使用实际值而不是路径参数。

@github-actions
Copy link
Copy Markdown
Contributor

Auto Review

No clear rule violations found in the current diff.

async function fetchTwitterPage(): Promise<string> {
const response = await ofetch('https://x.com', {
headers: {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use this fixed version of UA instead of RSSHub's auto-generated UA string? Does the site only works with this specific version of UA string?

scriptUrls.map(async (url) => {
const content = await ofetch(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use this fixed version of UA instead of RSSHub's auto-generated UA string? Does the site only works with this specific version of UA string?


if (Object.keys(ids).length > 0) {
try {
await cache.set(CACHE_KEY, JSON.stringify(ids), CACHE_TTL);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not hard-coded cache duration. Users are expected to change it to their likings through CACHE_CONTENT_EXPIRE

Comment on lines +136 to +140
try {
await cache.set(CACHE_KEY, JSON.stringify(ids), CACHE_TTL);
} catch {
logger.debug('twitter gql-id-resolver: cache write failed, IDs will be refetched next time');
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not wrap cache.set with try-catch.

} catch {
logger.debug('twitter gql-id-resolver: failed to parse chunk map');
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main.hash.js has already got those 10 graphql hashes. Why other files are still needed?

Comment on lines +132 to +143
logger.info('twitter gql-id-resolver: fetching fresh query IDs from Twitter JS bundles');
const ids = await fetchAndExtractIds();

if (Object.keys(ids).length > 0) {
try {
await cache.set(CACHE_KEY, JSON.stringify(ids), CACHE_TTL);
} catch {
logger.debug('twitter gql-id-resolver: cache write failed, IDs will be refetched next time');
}
const found = operationNames.filter((name) => ids[name]);
const missing = operationNames.filter((name) => !ids[name]);
logger.info(`twitter gql-id-resolver: resolved ${found.length}/${operationNames.length} query IDs. Missing: ${missing.join(', ') || 'none'}`);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use debug log level

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto: route no found Automated test failed due to route can not be found in PR description body route

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Twitter API UserTweetsAndReplies 404 Not Found

2 participants