Merge pull request #3914 from HeyPuter/juancastro/put-1869-unhandled-rejection-on-the-hottest-permission-path-can-take

fix: stop leaking an unawaited permission traversal on every check
This commit is contained in:
Juan Fernando Castro
2026-09-22 14:28:44 -04:00
committed by GitHub
2 changed files with 33 additions and 6 deletions
@@ -1712,6 +1712,32 @@ describe('PermissionService — scan paths', () => {
expect(await permService.check(actor, permission)).toBe(false);
});
// The traversal used to run beside the flat read with nobody awaiting it.
it('answers a flat hit without running the linked traversal', async () => {
const { row, actor } = await makeGroupedUser();
const permission = `zztest:flat-${uuidv4()}:ii:read`;
await server.stores.permission.setFlatUserPerm(row.id, permission, {
permission,
deleted: false,
issuer_user_id: row.id,
} as never);
const linked = vi
.spyOn(server.stores.permission, 'readLinkedUserUserPerms')
.mockRejectedValue(new Error('db wobble'));
try {
const reading = await permService.validateUserPerms({
actor,
permissions: [permission],
});
expect(reading).toHaveLength(1);
expect(reading[0]).toMatchObject({ permission });
expect(linked).not.toHaveBeenCalled();
} finally {
linked.mockRestore();
}
});
it('returns nothing for an actor with no user id', async () => {
expect(
await permService.validateUserPerms({
@@ -705,19 +705,20 @@ export class PermissionService extends PuterService {
}): Promise<ReadingNode[]> {
if (!actor.user?.id) return [];
const flatPromise = this.#flatValidateUserPerms(actor, permissions);
const linkedPromise = this.#linkedValidateUserPerms(
const flatReading = await this.#flatValidateUserPerms(
actor,
permissions,
state ?? { antiCycleActors: [actor] },
);
const flatReading = await flatPromise;
if (flatReading.length > 0) {
return flatReading[0].deleted ? [] : flatReading;
}
const linkedReading = await linkedPromise;
// Only on a miss: started beside the flat read, nothing awaits its rejection.
const linkedReading = await this.#linkedValidateUserPerms(
actor,
permissions,
state ?? { antiCycleActors: [actor] },
);
const flatOptions = PermissionUtil.readingToOptions(linkedReading);
// Warm flat KV cache for future hits (fire-and-forget, don't block