fix(patreon): full-fetch fallback when sparse fieldset returns null content (#842)
Operator-flagged: 9 StickySpoodge posts had empty bodies in FC despite the body plainly existing + being accessible (creds refresh didn't help). All 9 are body-only / poll / embed / announcement posts with no downloadable gallery media — Patreon's detail endpoint returns content:null for these under the sparse fields[post]=content request even though the body exists. fetch_post_detail_content now re-fetches the FULL post resource once when the sparse request comes back empty: recovers the body when the sparse fieldset was the cause, and logs post_type when even the full resource is empty (body lives elsewhere). Only the empty cases pay the extra GET; the 126 already-working posts keep the fast sparse path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -612,12 +612,39 @@ class PatreonClient:
|
||||
if isinstance(content, str) and content.strip():
|
||||
log.info("post-detail: fetched %d chars (post %s)", len(content), post_id)
|
||||
return content
|
||||
# 200 OK but content is null/empty — the common silent case (a tier-gated
|
||||
# post serves content:null, or the post genuinely has no text body). Logged
|
||||
# so a recapture that "caught nothing" is diagnosable, not invisible.
|
||||
# Sparse `fields[post]=content` came back null/empty. Patreon serves null
|
||||
# under the sparse fieldset for some post types (polls, embeds/films,
|
||||
# body-only announcements) even when the body plainly exists. Re-fetch the
|
||||
# FULL post resource ONCE — only the empty cases pay this extra GET — which
|
||||
# both DIAGNOSES (logs post_type) and RECOVERS the body when the sparse
|
||||
# fieldset was the cause (#842).
|
||||
try:
|
||||
full = self._session.get(
|
||||
url, params={"json-api-version": "1.0"}, timeout=_TIMEOUT_SECONDS,
|
||||
)
|
||||
except requests.RequestException as exc:
|
||||
log.warning("post-detail full re-fetch failed (post %s): %s", post_id, exc)
|
||||
return None
|
||||
fa = None
|
||||
if full.status_code == 200:
|
||||
try:
|
||||
fp = full.json()
|
||||
except ValueError:
|
||||
fp = None
|
||||
fdata = fp.get("data") if isinstance(fp, dict) else None
|
||||
fa = fdata.get("attributes") if isinstance(fdata, dict) else None
|
||||
fcontent = fa.get("content") if isinstance(fa, dict) else None
|
||||
ptype = fa.get("post_type") if isinstance(fa, dict) else None
|
||||
if isinstance(fcontent, str) and fcontent.strip():
|
||||
log.warning(
|
||||
"post-detail: sparse fieldset gave null but FULL fetch has %d chars "
|
||||
"(post %s, post_type=%s) — using full body",
|
||||
len(fcontent), post_id, ptype,
|
||||
)
|
||||
return fcontent
|
||||
log.info(
|
||||
"post-detail: empty/null content (post %s) — body unavailable "
|
||||
"(tier-gated, or post has no text)", post_id,
|
||||
"post-detail: empty/null content (post %s, post_type=%s) even on full "
|
||||
"fetch — body not in the post resource", post_id, ptype,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@@ -380,6 +380,28 @@ def test_fetch_post_detail_content_empty_body_is_none(monkeypatch):
|
||||
assert client.fetch_post_detail_content("42") is None
|
||||
|
||||
|
||||
def test_fetch_post_detail_content_falls_back_to_full_fetch(monkeypatch):
|
||||
"""#842: when the sparse fields[post]=content request returns null (Patreon
|
||||
does this for polls / embeds / body-only posts), a one-time FULL re-fetch
|
||||
recovers the body — so inline-only / no-media posts still get their text."""
|
||||
client = PatreonClient(cookies_path=None)
|
||||
seen_params: list = []
|
||||
|
||||
def _get(url, params=None, timeout=None):
|
||||
seen_params.append(params or {})
|
||||
if "fields[post]" in (params or {}):
|
||||
return _FakeResp(200, json_data={"data": {"attributes": {"content": None}}})
|
||||
return _FakeResp(200, json_data={
|
||||
"data": {"attributes": {"content": "<p>body via full</p>", "post_type": "poll"}}
|
||||
})
|
||||
|
||||
monkeypatch.setattr(client._session, "get", _get)
|
||||
assert client.fetch_post_detail_content("42") == "<p>body via full</p>"
|
||||
# Sparse request first, then a full re-fetch without the sparse fieldset.
|
||||
assert "fields[post]" in seen_params[0]
|
||||
assert "fields[post]" not in seen_params[1]
|
||||
|
||||
|
||||
def test_fetch_post_detail_content_blank_id_skips_request(monkeypatch):
|
||||
calls: list = []
|
||||
client = PatreonClient(cookies_path=None)
|
||||
|
||||
Reference in New Issue
Block a user