fix: 431 Request Too Large — reduce FEATURES to 15 essential keys, dynamic update only updates existing keys

This commit is contained in:
jackwener
2026-03-09 20:59:16 +08:00
parent d20c5699fd
commit fda9b1c3dc
4 changed files with 25 additions and 46 deletions

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "twitter-cli" name = "twitter-cli"
version = "0.4.2" version = "0.4.3"
description = "A CLI for Twitter/X — feed, bookmarks, and user timeline in terminal" description = "A CLI for Twitter/X — feed, bookmarks, and user timeline in terminal"
readme = "README.md" readme = "README.md"
license = "Apache-2.0" license = "Apache-2.0"

View File

@@ -213,30 +213,28 @@ class TestBestChromeTarget:
# ── _update_features_from_html ─────────────────────────────────────────── # ── _update_features_from_html ───────────────────────────────────────────
class TestUpdateFeaturesFromHtml: class TestUpdateFeaturesFromHtml:
def test_extracts_feature_flags(self): def test_updates_existing_feature_flags(self):
# Save original state """Should update existing FEATURES keys, not add new ones."""
original = dict(FEATURES) original = dict(FEATURES)
try: try:
html = ''' # Use a key that exists in FEATURES
"responsive_web_test_feature":{"value":true}, existing_key = list(FEATURES.keys())[0]
"responsive_web_another_feature":{"value":false}, original_value = FEATURES[existing_key]
"rweb_some_flag":{"value":true} opposite = "false" if original_value else "true"
''' html = '"%s":{"value":%s}' % (existing_key, opposite)
_update_features_from_html(html) _update_features_from_html(html)
assert FEATURES["responsive_web_test_feature"] is True assert FEATURES[existing_key] != original_value
assert FEATURES["responsive_web_another_feature"] is False
assert FEATURES["rweb_some_flag"] is True
finally: finally:
# Restore original state
FEATURES.clear() FEATURES.clear()
FEATURES.update(original) FEATURES.update(original)
def test_ignores_non_feature_keys(self): def test_does_not_add_new_keys(self):
"""Should never add keys not already in FEATURES (prevents URL bloat)."""
original = dict(FEATURES) original = dict(FEATURES)
try: try:
html = '"some_random_key":{"value":true}' html = '"responsive_web_brand_new_feature":{"value":true}'
_update_features_from_html(html) _update_features_from_html(html)
assert "some_random_key" not in FEATURES assert "responsive_web_brand_new_feature" not in FEATURES
finally: finally:
FEATURES.clear() FEATURES.clear()
FEATURES.update(original) FEATURES.update(original)

View File

@@ -53,43 +53,24 @@ TWITTER_OPENAPI_URL = (
"main/src/config/placeholder.json" "main/src/config/placeholder.json"
) )
# Essential features only — keep this list SMALL to avoid 414/431 URI Too Long.
# Twitter's API defaults missing features to False, so we only need True-valued ones
# that affect tweet data we actually consume. Each additional key adds ~60 chars to URL.
_DEFAULT_FEATURES = { _DEFAULT_FEATURES = {
"rweb_video_screen_enabled": False,
"profile_label_improvements_pcf_label_in_post_enabled": True,
"responsive_web_profile_redirect_enabled": False,
"rweb_tipjar_consumption_enabled": False,
"verified_phone_label_enabled": False,
"creator_subscriptions_tweet_preview_api_enabled": True, "creator_subscriptions_tweet_preview_api_enabled": True,
"responsive_web_graphql_timeline_navigation_enabled": True,
"responsive_web_graphql_skip_user_profile_image_extensions_enabled": False,
"premium_content_api_read_enabled": False,
"communities_web_enable_tweet_community_results_fetch": True, "communities_web_enable_tweet_community_results_fetch": True,
"c9s_tweet_anatomy_moderator_badge_enabled": True, "c9s_tweet_anatomy_moderator_badge_enabled": True,
"responsive_web_grok_analyze_button_fetch_trends_enabled": False,
"responsive_web_grok_analyze_post_followups_enabled": True,
"responsive_web_jetfuel_frame": True,
"responsive_web_grok_share_attachment_enabled": True,
"responsive_web_grok_annotations_enabled": True,
"articles_preview_enabled": True, "articles_preview_enabled": True,
"responsive_web_edit_tweet_api_enabled": True, "responsive_web_edit_tweet_api_enabled": True,
"graphql_is_translatable_rweb_tweet_is_translatable_enabled": True, "graphql_is_translatable_rweb_tweet_is_translatable_enabled": True,
"view_counts_everywhere_api_enabled": True, "view_counts_everywhere_api_enabled": True,
"longform_notetweets_consumption_enabled": True, "longform_notetweets_consumption_enabled": True,
"responsive_web_twitter_article_tweet_consumption_enabled": True, "responsive_web_twitter_article_tweet_consumption_enabled": True,
"tweet_awards_web_tipping_enabled": False,
"content_disclosure_indicator_enabled": True,
"content_disclosure_ai_generated_indicator_enabled": True,
"responsive_web_grok_show_grok_translated_post": True,
"responsive_web_grok_analysis_button_from_backend": True,
"post_ctas_fetch_enabled": True,
"freedom_of_speech_not_reach_fetch_enabled": True,
"standardized_nudges_misinfo": True,
"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": True, "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": True,
"longform_notetweets_rich_text_read_enabled": True, "longform_notetweets_rich_text_read_enabled": True,
"longform_notetweets_inline_media_enabled": False, "freedom_of_speech_not_reach_fetch_enabled": True,
"responsive_web_grok_image_annotation_enabled": True, "standardized_nudges_misinfo": True,
"responsive_web_grok_imagine_annotation_enabled": True, "responsive_web_graphql_timeline_navigation_enabled": True,
"responsive_web_grok_community_note_auto_translation_is_enabled": False,
"responsive_web_enhance_cards_enabled": False, "responsive_web_enhance_cards_enabled": False,
} }
@@ -226,11 +207,9 @@ def _update_features_from_html(html):
Twitter embeds feature switch config in inline scripts on the homepage. Twitter embeds feature switch config in inline scripts on the homepage.
We parse these to keep FEATURES in sync with the current frontend. We parse these to keep FEATURES in sync with the current frontend.
Only UPDATES existing keys — never adds new ones to avoid URL bloat.
""" """
try: try:
# Look for feature flags in inline script content
# Pattern: "featureSwitch":{"...":{"value":true/false},...}
# Also try: features:{key:!0, key2:!1, ...} in JS bundles
feature_pattern = re.compile( feature_pattern = re.compile(
r'"([a-z][a-z0-9_]+)":\s*\{\s*"value"\s*:\s*(true|false)', r'"([a-z][a-z0-9_]+)":\s*\{\s*"value"\s*:\s*(true|false)',
re.IGNORECASE, re.IGNORECASE,
@@ -239,8 +218,10 @@ def _update_features_from_html(html):
for match in feature_pattern.finditer(html): for match in feature_pattern.finditer(html):
key = match.group(1) key = match.group(1)
value = match.group(2).lower() == "true" value = match.group(2).lower() == "true"
# Only update keys that look like feature flags # Only update keys already in FEATURES — never add new ones
if any(prefix in key for prefix in ("responsive_web_", "rweb_", "longform_", "creator_", "communities_", "c9s_")): # Adding new keys inflates URL length, causing 414/431 errors
if key in FEATURES and FEATURES[key] != value:
logger.debug("Feature flag updated: %s = %s -> %s", key, FEATURES[key], value)
FEATURES[key] = value FEATURES[key] = value
found += 1 found += 1
if found: if found:

2
uv.lock generated
View File

@@ -950,7 +950,7 @@ wheels = [
[[package]] [[package]]
name = "twitter-cli" name = "twitter-cli"
version = "0.4.1" version = "0.4.2"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "beautifulsoup4" }, { name = "beautifulsoup4" },