fix(tiktok): the handle lookup gave up whenever its answer was cached

The 5.0.2 fix worked when I tested it and stopped working once I had used
it, which is the shape of a bug that is better at hiding than at
happening.

`FetchService.fetch` answers a cache hit straight from storage and never
calls the `parse` callback. The lookup collected its author through that
callback, so the very first resolution of a handle succeeded - cold cache,
real fetch, callback runs - and every one inside the next fifteen minutes
found an empty list and gave up. Giving up is silent by design there: it
leaves the original refusal standing rather than replacing it with an
error about a call the caller never made. So the failure was a return to
exactly the message it was supposed to remove, with nothing in the log
and no upstream request to notice.

It parses `result.payload` now, which is populated either way.

Two log lines were missing for the same reason I could not see this from
the outside: the no-id branch said nothing at all, and the resolved line
now carries `cached` so the two paths are distinguishable in a log.

The regression test is the cache-hit shape specifically - payload set,
callback never invoked - because a test that only exercises a real fetch
is the test that passed while this shipped.
This commit is contained in:
Evil0ctal
2026-09-11 01:31:23 -07:00
parent 9d213830f1
commit bbd08152d3
2 changed files with 43 additions and 12 deletions
+16 -11
View File
@@ -626,21 +626,14 @@ class TaskWorker:
handle = value.lstrip("@")
lookup = registry.resolve(profile_endpoint, {registry.UNIQUE_ID: handle}, self._config())
found: list[Any] = []
def parse(payload: dict[str, Any]) -> Any:
model = lookup.parse(payload)
found.append(model)
return model
try:
async with self._session_factory() as session:
await self._fetch.fetch(
result = await self._fetch.fetch(
session,
lookup.platform,
lookup.endpoint,
lookup.params,
parse=parse,
parse=lookup.parse,
cache_ttl=lookup.cache_ttl,
ctx=FetchContext(
task_id=run.id,
@@ -652,6 +645,12 @@ class TaskWorker:
is_demo=run.is_demo,
),
)
# Parsed from what came back, not collected from the `parse`
# callback. A cache hit returns the stored payload and never calls
# it, so a callback-collected author was present on the first
# lookup of a handle and absent on every one after - which is a
# failure that gets better when you stop looking at it.
author = lookup.parse(result.payload)
except DtkError as exc:
log.info(
"worker.author_handle.lookup_failed",
@@ -661,11 +660,17 @@ class TaskWorker:
)
return params
sec_uid = getattr(found[0], "sec_uid", None) if found else None
sec_uid = getattr(author, "sec_uid", None)
if not isinstance(sec_uid, str) or not sec_uid:
log.info("worker.author_handle.no_id", endpoint=endpoint, handle=handle)
return params
log.info("worker.author_handle.resolved", endpoint=endpoint, handle=handle)
log.info(
"worker.author_handle.resolved",
endpoint=endpoint,
handle=handle,
cached=result.cached,
)
resolved = dict(params)
resolved.pop("sec_user_id", None)
resolved[registry.AUTHOR_ID] = sec_uid
+27 -1
View File
@@ -1768,7 +1768,10 @@ async def test_a_tiktok_handle_is_resolved_before_the_call_that_cannot_use_it()
first - advice that led nowhere, because the profile it would have returned
carried no usable id either. Now the worker does the lookup itself.
"""
fetch = FakeFetch(parse_payload=_tiktok_profile_payload())
# Both set: a fresh fetch returns the payload *and* runs the callback. The
# cache-hit case, which only returns the payload, is the test below.
payload = _tiktok_profile_payload()
fetch = FakeFetch(payload=payload, parse_payload=payload)
worker, _ = make_worker(FakeStore(), fetch)
run = a_run("tiktok.author_posts", sec_user_id="owlcitymusic")
@@ -1781,6 +1784,29 @@ async def test_a_tiktok_handle_is_resolved_before_the_call_that_cannot_use_it()
assert "sec_user_id" not in resolved
async def test_a_cached_profile_still_yields_the_id() -> None:
"""The lookup must read what came back, not what the callback collected.
`FetchService.fetch` returns a cache hit straight from storage and never
calls `parse`. Collecting the author through that callback therefore worked
on the first lookup of a handle and failed on every one afterwards - a bug
that got better when you stopped looking at it, and the reason the fix
shipped in 5.0.2 appeared to work when it was tested and not when it was
used.
"""
# payload set, parse_payload left None: FakeFetch answers without ever
# invoking the callback, which is exactly what a cache hit does.
fetch = FakeFetch(payload=_tiktok_profile_payload())
worker, _ = make_worker(FakeStore(), fetch)
run = a_run("tiktok.author_posts", sec_user_id="owlcitymusic")
resolved = await worker._resolve_author_handle("tiktok.author_posts", dict(run.params), run)
assert len(fetch.calls) == 1
assert resolved["author_id"].startswith("MS4wLjAB")
assert "sec_user_id" not in resolved
async def test_an_id_that_is_already_a_sec_uid_costs_no_lookup() -> None:
"""The common case must not pay for a request it does not need."""
fetch = FakeFetch()