Add Discord API rate limit handling and bump @pulumi/github (#84)

- Handle 429 responses from Discord API with automatic retry based on
  retry_after, preventing transient failures during role sync
- Surface underlying error details in role and member role read failures
  to make debugging easier
- Bump @pulumi/github to ^6.11.0 (resolves 6.12.1)

🏠 Remote-Dev: homespace
This commit is contained in:
Den Delimarsky 2026-03-20 01:08:13 -07:00 • committed by GitHub
commit 3a06068d55
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 56 additions and 24 deletions

8
package-lock.json generated
View file

@ -9,7 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@pulumi/github": "^6.7.3", "@pulumi/github": "^6.11.0",
"@pulumi/googleworkspace": "file:sdks/googleworkspace", "@pulumi/googleworkspace": "file:sdks/googleworkspace",
"@pulumi/pulumi": "^3.218.0", "@pulumi/pulumi": "^3.218.0",
"@pulumi/random": "^4.14.0" "@pulumi/random": "^4.14.0"
@ -655,9 +655,9 @@
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/@pulumi/github": { "node_modules/@pulumi/github": {
"version": "6.9.1", "version": "6.12.1",
"resolved": "https://registry.npmjs.org/@pulumi/github/-/github-6.9.1.tgz", "resolved": "https://registry.npmjs.org/@pulumi/github/-/github-6.12.1.tgz",
"integrity": "sha512-B7FcceruXfMClws4poIyUfJoy2dqcdhi8P5+ahfArIeaDjFDlcRNU9SZbfFRbkdoXflMJi+AI3rWabyvqS2+YQ==", "integrity": "sha512-OsbQgF8go/tA1PuMhFq7O0XVfYbwRk0/LHaZTH6OFO395SufdFFa1go3IeXvJCSA5oDCbdULerGg4n9sP8vTLg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@pulumi/pulumi": "^3.142.0" "@pulumi/pulumi": "^3.142.0"

View file

@ -20,31 +20,63 @@ interface DiscordApiError {
message: string; message: string;
} }
interface DiscordRateLimitResponse {
message: string;
retry_after: number;
global: boolean;
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function discordFetch<T>( async function discordFetch<T>(
token: string, token: string,
endpoint: string, endpoint: string,
options: RequestInit = {} options: RequestInit = {},
maxRetries = 5
): Promise<T> { ): Promise<T> {
const response = await fetch(`${DISCORD_API_BASE}${endpoint}`, { let lastError: Error | undefined;
...options,
headers: {
Authorization: `Bot ${token}`,
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) { for (let attempt = 0; attempt <= maxRetries; attempt++) {
const error = (await response.json()) as DiscordApiError; const response = await fetch(`${DISCORD_API_BASE}${endpoint}`, {
throw new Error(`Discord API error: ${error.message} (code: ${error.code})`); ...options,
headers: {
Authorization: `Bot ${token}`,
'Content-Type': 'application/json',
...options.headers,
},
});
if (response.status === 429) {
const body = (await response.json()) as DiscordRateLimitResponse;
const retryAfterMs = Math.ceil(body.retry_after * 1000) + Math.random() * 250;
lastError = new Error(
`Discord API rate limited on ${endpoint} (retry_after=${body.retry_after}s, global=${body.global})`
);
if (attempt < maxRetries) {
await sleep(retryAfterMs);
continue;
}
throw lastError;
}
if (!response.ok) {
const error = (await response.json()) as DiscordApiError;
throw new Error(`Discord API error: ${error.message} (code: ${error.code})`);
}
// Handle 204 No Content
if (response.status === 204) {
return undefined as T;
}
return response.json() as Promise<T>;
} }
// Handle 204 No Content throw (
if (response.status === 204) { lastError ?? new Error(`Discord API request to ${endpoint} failed after ${maxRetries} retries`)
return undefined as T; );
}
return response.json() as Promise<T>;
} }
// Discord API response types // Discord API response types
@ -122,8 +154,8 @@ const discordRoleProvider: pulumi.dynamic.ResourceProvider = {
roleId: role.id, roleId: role.id,
}, },
}; };
} catch { } catch (error) {
throw new Error(`Failed to read role ${id}`); throw new Error(`Failed to read role ${id}: ${error}`);
} }
}, },
@ -303,7 +335,7 @@ const discordMemberRoleSyncProvider: pulumi.dynamic.ResourceProvider = {
}, },
}; };
} }
throw new Error(`Failed to read member roles for ${id}`); throw new Error(`Failed to read member roles for ${id}: ${error}`);
} }
const currentRoles = new Set(member.roles); const currentRoles = new Set(member.roles);