mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-12 21:02:17 +02:00
fix(apps): pace device-flow polls in core — slow_down made auth spin forever
GitHub rate-limits the device-code token endpoint: a poll arriving even slightly under the flow interval returns slow_down and permanently raises the required interval. The renderer polled on a fixed 5s timer (exactly the limit), so one jittery request tripped slow_down and every poll after it was 'too fast' — GitHub answered slow_down forever and the dialog sat on 'Waiting for authorization' even after the user approved. Core now tracks lastTokenPollAt and skips the request until the flow's current interval (base +1s margin) has elapsed, so slow_down bumps take effect and the flow recovers. The renderer timer is just a heartbeat (2s).
This commit is contained in:
parent
5a7fd1506c
commit
b55417e97c
2 changed files with 21 additions and 2 deletions
|
|
@ -62,7 +62,9 @@ export function PublishDialog({ folder, appName, onClose, onPublished }: {
|
|||
setError(e instanceof Error ? e.message : String(e))
|
||||
setPhase('auth')
|
||||
})
|
||||
}, 5000)
|
||||
// Heartbeat only — core paces the actual GitHub requests to the
|
||||
// flow's required interval (and skips the request when it's too soon).
|
||||
}, 2000)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ type PendingFlow = {
|
|||
deviceCode: string;
|
||||
intervalMs: number;
|
||||
expiresAt: number;
|
||||
/** Last time we actually hit GitHub's token endpoint (pacing). */
|
||||
lastTokenPollAt?: number;
|
||||
/** Token already issued but the identity fetch failed — retry that step. */
|
||||
issuedToken?: string;
|
||||
identityAttempts?: number;
|
||||
|
|
@ -71,7 +73,10 @@ export async function startDeviceFlow(): Promise<{ userCode: string; verificatio
|
|||
};
|
||||
pending = {
|
||||
deviceCode: body.device_code,
|
||||
intervalMs: (body.interval || 5) * 1000,
|
||||
// +1s safety margin: GitHub measures arrival spacing, and a request
|
||||
// that lands even slightly early gets `slow_down`, which RAISES the
|
||||
// required interval for the rest of the flow.
|
||||
intervalMs: ((body.interval || 5) + 1) * 1000,
|
||||
expiresAt: Date.now() + body.expires_in * 1000,
|
||||
};
|
||||
return { userCode: body.user_code, verificationUri: body.verification_uri, expiresIn: body.expires_in };
|
||||
|
|
@ -93,6 +98,18 @@ export async function pollDeviceFlow(): Promise<PollResult> {
|
|||
|
||||
let accessToken = pending.issuedToken;
|
||||
if (!accessToken) {
|
||||
// Pacing lives HERE, not in the renderer: the renderer's timer is just
|
||||
// a heartbeat. GitHub rate-limits the token endpoint per device code —
|
||||
// polling faster than the flow's interval returns `slow_down` and
|
||||
// permanently raises the required interval, and a caller that keeps
|
||||
// its own fixed cadence then gets `slow_down` on EVERY poll (looks
|
||||
// "pending" forever, even after the user authorized). Skip the request
|
||||
// entirely until the current interval has elapsed.
|
||||
const now = Date.now();
|
||||
if (pending.lastTokenPollAt !== undefined && now - pending.lastTokenPollAt < pending.intervalMs) {
|
||||
return { status: 'pending' };
|
||||
}
|
||||
pending.lastTokenPollAt = now;
|
||||
const res = await fetch('https://github.com/login/oauth/access_token', {
|
||||
method: 'POST',
|
||||
headers: { ...GH_HEADERS, 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue