[Medium] Intel resolver caches provider failures as successful empty entity graphs for 24 hours
Severity: Medium (cache poisoning / availability) Confidence: Confirmed by an executed zero-network reproduction of the actual resolver and route. Audited commit: bd4057567de8ee18d49a8b2744c9746bcd67f3e0 (master, 2026-09-13 audit).
Entity-resolution results are cached for 24 hours the moment a resolver returns, with no distinction between "genuinely no relationships found" and "upstream provider failed/errored." A single transient Wikidata outage during an entity's first lookup poisons that entity's cache as an empty result for the full 24-hour TTL — recovery of the upstream provider has no effect until the cache entry naturally expires.
Evidence and mechanism
- intel/server.js:29-31,149-164: cached resolver results are kept for 24 hours regardless of how they were produced.
- Lines 185, 200-203: Wikidata HTTP errors are converted into empty bindings/null search results rather than propagated as failures.
- Lines 388-425 (and equivalently at 334-335, 376-378, 466-468, 599-600, 680-682): every resolver converts an upstream failure into an empty graph and caches it unconditionally, with no failure/success distinction.
- Lines 743-751 and src/app/api/entity/expand/route.ts:64-67: the empty result is returned as HTTP 200 with public, shared-cache headers.
Reproduction and observed result
- Mocked Wikidata search/SPARQL as HTTP 503 and resolved
company:Example Companythrough the actual route: HTTP 200, zero graph nodes. - Changed the mock to return a valid relationship and immediately repeated the identical request: still zero nodes, zero new upstream calls made — confirming the empty result was served from cache, not re-fetched.
- Cleared only the resolver's internal cache and repeated the same request: the (now-working) upstream produced one graph node, confirming the provider was never actually broken — only the cached response was.
{"firstStatus":200,"recoveredProviderNodes":0,"requestsWhileCached":0,"afterCacheClearNodes":1,"cacheHeader":"public, s-maxage=3600, stale-while-revalidate=7200"}Impact
Any entity whose first lookup happens to coincide with a brief Wikidata rate-limit or outage is marked as having no relationships for a full day, and that false-empty response can additionally be cached by shared HTTP infrastructure due to the public cache-control header.
Smallest correction and acceptance criteria
- Distinguish "successfully resolved, no relationships" from "resolution failed" throughout every resolver, and cache only the former.
- Do not attach long-lived
publiccache headers to failed/partial responses; if partial results are intentionally retained, mark them with explicit degraded/stale metadata and a much shorter or no cache policy. - Acceptance: a mocked provider failure followed by a mocked recovery must produce a fresh (non-cached) successful result on the very next request after recovery, without needing to wait out the TTL.
Prior-issue check: #15/#153 (closed) describe general exception swallowing elsewhere; #39 addresses static-data provenance. No existing issue covers this resolver cache-poisoning behavior.
Source: simplifaisoul/osiris