From 67b13c42d32a20ebdd0a4e95ea794e418a434158 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 25 Oct 2026 16:40:54 +0100 Subject: [PATCH] nextcloud: repair the account on a throttle too, since a throttle follows refusals (issue 1253) Right after the delivery the first answer may be Nextcloud's 429, not a 401; without a repair then the account would never be made and the back-off would wait out a throttle that cannot end. --- modules/nextcloud/client.ts | 10 ++++++---- modules/nextcloud/test/admin-login.test.ts | 8 +++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/modules/nextcloud/client.ts b/modules/nextcloud/client.ts index 91a79c5a..f3d1f725 100644 --- a/modules/nextcloud/client.ts +++ b/modules/nextcloud/client.ts @@ -289,13 +289,15 @@ export class NextcloudClient { } /** - * List every share, over the OCS Sharing API as the mesh's own account. A refused login (401) - * makes the account right once (ensureAdminAccount) and asks again; a refusal after that, or a - * throttle (429), throws LoginRefusedError, so the caller backs off (novox/hq issue 1253). + * List every share, over the OCS Sharing API as the mesh's own account. A refused login (401) or + * a throttle (429) makes the account right once (ensureAdminAccount) and asks again; a refusal + * or a throttle after that throws LoginRefusedError, so the caller backs off (novox/hq issue 1253). */ async listShares(): Promise { let res = await this.fetchShares(); - if (res.status === 401 && !this.accountMadeRight) { + // A throttle (429) follows refusals, so it gets the same one repair: the repair goes by occ, + // never by a login, and the throttle then wears off under the caller's back-off. + if ((res.status === 401 || res.status === 429) && !this.accountMadeRight) { await res.text(); this.accountMadeRight = true; this.ensureAdminAccount(); diff --git a/modules/nextcloud/test/admin-login.test.ts b/modules/nextcloud/test/admin-login.test.ts index 08e2d1e7..1e38a22b 100644 --- a/modules/nextcloud/test/admin-login.test.ts +++ b/modules/nextcloud/test/admin-login.test.ts @@ -113,15 +113,17 @@ test("a refusal after the repair throws LoginRefusedError once-repaired, short a } }); -// 1253/AC3b: a throttle (429) is a refusal too, and is not met by a repair. -test("a throttled login (429) throws LoginRefusedError without touching the account", async () => { +// 1253/AC3b: a throttle (429) follows refusals: it gets the same one repair, and a throttle +// that stays throws LoginRefusedError. +test("a throttled login (429) is repaired once and then throws LoginRefusedError", async () => { const c = client(); const occ = standInOcc(true); mock.method(c, "occ", occ.impl); const f = mock.method(globalThis, "fetch", standInFetch([429]).impl); try { await assert.rejects(c.listShares(), (err: any) => err instanceof LoginRefusedError && err.status === 429); - assert.equal(occ.calls.length, 0); + await assert.rejects(c.listShares(), LoginRefusedError); + assert.equal(occ.calls.filter((a) => a[0] === "user:resetpassword").length, 1); } finally { f.mock.restore(); }