Skip to content

resolveLocalizationContext's 30s failure cache can answer a deliberate post-bind re-read with the pre-bind value #11877

Description

@claude

Found while implementing #11622 (the MCP stdio pre-bind localization memo). Not fixed there — the code is in @objectstack/core, outside that card's declared file surface, and narrowing it touches a shared read path with its own dogfood pin.

The mechanism

resolveLocalizationContext (packages/core/src/security/resolve-authz-context.ts:798) memoizes an outcome for LOCALIZATION_FAILURE_CACHE_TTL_MS = 30s, keyed on (ql, "tenantId|userId"), whenever the underlying read reports failed (#10221, so a repeatedly-failing sys_setting query does not re-run and re-log every request):

780 const LOCALIZATION_FAILURE_CACHE_TTL_MS = 30_000;
803 if (hit && hit.expiresAt > Date.now()) return hit.value;

failed is set in six places, and only two of them are the backend fault the cache's own docblock describes. The other four are the settings service leg throwing (resolve-authz-context.ts:853, 858, 862, 866) — including the all-or-nothing refusal SettingsService.getMany gives for a localization namespace whose manifest is not (yet) fully registered, which the caller's own comment at line 834 already records as a known degradation.

Why that matters now

It makes the 30s cache reachable inside the settings pre-bind window — and a caller that deliberately re-reads after the bind can then be served the cached pre-bind answer. #11622's repair is exactly such a caller: it discards the memo taken inside the window and re-resolves at kernel:bootstrapped. If the in-window read had set failed, that re-resolution can be answered from the cache within 30s and then memoized as the post-bind value — the correction silently not happening, with nothing in the output saying so.

The same interaction predates #11622 in a worse form (before it, the pre-bind answer was kept unconditionally and forever), so this is a residue of that repair rather than a regression from it.

Scope and honesty about what was measured

Not measured.#11622's pins do not reach it and were not written to: the settings double there never throws and sys_setting answers [], so failed stays false on every leg. This is filed from reading the cache's write condition against its docblock, not from a repro.

Two candidate repairs, both with a cost worth deciding deliberately rather than folding into a bug fix:

  • Only cache a genuine backend fault (the direct ql.find throw), not a settings-service throw. Closest to the cache's stated intent; loses the de-dup for the settings-throw case that also spams.
  • Give the caller a way to bypass the entry on a read it knows is a deliberate re-read after a state change.

Either one moves a read path pinned by packages/qa/dogfood/test/analytics-timezone.dogfood.test.ts (the #1982/#2018 golden regression, which is why the cache was narrowed to failures in the first place), so it wants its own card.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions