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

View file

@ -20,31 +20,63 @@ interface DiscordApiError {
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>(
token: string,
endpoint: string,
options: RequestInit = {}
options: RequestInit = {},
maxRetries = 5
): Promise<T> {
const response = await fetch(`${DISCORD_API_BASE}${endpoint}`, {
...options,
headers: {
Authorization: `Bot ${token}`,
'Content-Type': 'application/json',
...options.headers,
},
});
let lastError: Error | undefined;
if (!response.ok) {
const error = (await response.json()) as DiscordApiError;
throw new Error(`Discord API error: ${error.message} (code: ${error.code})`);
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(`${DISCORD_API_BASE}${endpoint}`, {
...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
if (response.status === 204) {
return undefined as T;
}
return response.json() as Promise<T>;
throw (
lastError ?? new Error(`Discord API request to ${endpoint} failed after ${maxRetries} retries`)
);
}
// Discord API response types
@ -122,8 +154,8 @@ const discordRoleProvider: pulumi.dynamic.ResourceProvider = {
roleId: role.id,
},
};
} catch {
throw new Error(`Failed to read role ${id}`);
} catch (error) {
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);