Summary
Split out of #637. PR #947 fixed the API-token leg and PR #970 fixes the identity/authorization leg, but three more sites collapse an availability failure into a definitive negative answer — the same defect class, each needing its own change.
An availability failure answered as a definitive negative is worse than an error, because it is actionable-looking: the user is told something specific and false, and acts on it. #637's headline case was a storage stall telling a signed-in user they were not onboarded and redirecting them to the sign-up form.
The remaining three
| Site |
Reports a … |
As |
AccessControlPipeline |
permission-fold fault |
"Access denied" |
MeshOperations.FetchNode |
read timeout |
"Not found" |
UserIdentityCache |
cold cache |
"user unknown" |
Each is a lie with a different cost. "Access denied" sends a correctly-entitled user to ask for permissions they already have. "Not found" invites deleting or recreating a node that exists. "User unknown" is the same trap #637 opened — it can drive onboarding or provisioning logic for a user who is already onboarded.
Why these are not a quick follow-up to #970
Unlike the identity reads, these cannot be fixed by classifying at a single bounded seam. Each needs either a tri-state carried through the permission fold (so "we could not determine" survives the fold instead of collapsing into "no") or a new ErrorType on the bus so the condition can cross a hub boundary without being flattened. That is why #970 deliberately stops short of them rather than reaching for a partial fix.
The rule these should follow
Decide the distinction where the condition is known — inside the one place a budget or a fold is applied — and never reconstruct it upstream by sniffing a message string. That is the shape #970 uses (IdentityReadOutcome<T> decided inside IdentityRead.Bounded), and it is what makes the classification survive rewording.
Also worth applying #970's lesson about its own fix: after removing a catch, sweep for edges that can now throw past the classifier. That sweep found a third instance in #970 itself — LoadDbRolesAsync resolving the hub and workspace outside the classified chain, where a hub mid-disposal would have escaped as a 500.
Not band-aids
Widening a timeout, retrying, or catching-and-defaulting are all excluded: the goal is to name the condition correctly, not to make it rarer.
Summary
Split out of #637. PR #947 fixed the API-token leg and PR #970 fixes the identity/authorization leg, but three more sites collapse an availability failure into a definitive negative answer — the same defect class, each needing its own change.
An availability failure answered as a definitive negative is worse than an error, because it is actionable-looking: the user is told something specific and false, and acts on it. #637's headline case was a storage stall telling a signed-in user they were not onboarded and redirecting them to the sign-up form.
The remaining three
AccessControlPipelineMeshOperations.FetchNodeUserIdentityCacheEach is a lie with a different cost. "Access denied" sends a correctly-entitled user to ask for permissions they already have. "Not found" invites deleting or recreating a node that exists. "User unknown" is the same trap #637 opened — it can drive onboarding or provisioning logic for a user who is already onboarded.
Why these are not a quick follow-up to #970
Unlike the identity reads, these cannot be fixed by classifying at a single bounded seam. Each needs either a tri-state carried through the permission fold (so "we could not determine" survives the fold instead of collapsing into "no") or a new
ErrorTypeon the bus so the condition can cross a hub boundary without being flattened. That is why #970 deliberately stops short of them rather than reaching for a partial fix.The rule these should follow
Decide the distinction where the condition is known — inside the one place a budget or a fold is applied — and never reconstruct it upstream by sniffing a message string. That is the shape #970 uses (
IdentityReadOutcome<T>decided insideIdentityRead.Bounded), and it is what makes the classification survive rewording.Also worth applying #970's lesson about its own fix: after removing a
catch, sweep for edges that can now throw past the classifier. That sweep found a third instance in #970 itself —LoadDbRolesAsyncresolving the hub and workspace outside the classified chain, where a hub mid-disposal would have escaped as a 500.Not band-aids
Widening a timeout, retrying, or catching-and-defaulting are all excluded: the goal is to name the condition correctly, not to make it rarer.