11import { resolveFetch } from "../infra/fetch.js" ;
2+ import { resolveRetryConfig , retryAsync , type RetryConfig } from "../infra/retry.js" ;
23
34const DISCORD_API_BASE = "https://discord.com/api/v10" ;
5+ const DISCORD_API_RETRY_DEFAULTS = {
6+ attempts : 3 ,
7+ minDelayMs : 500 ,
8+ maxDelayMs : 30_000 ,
9+ jitter : 0.1 ,
10+ } ;
411
512type DiscordApiErrorPayload = {
613 message ?: string ;
@@ -21,6 +28,19 @@ function parseDiscordApiErrorPayload(text: string): DiscordApiErrorPayload | nul
2128 return null ;
2229}
2330
31+ function parseRetryAfterSeconds ( text : string , response : Response ) : number | undefined {
32+ const payload = parseDiscordApiErrorPayload ( text ) ;
33+ const retryAfter =
34+ payload && typeof payload . retry_after === "number" && Number . isFinite ( payload . retry_after )
35+ ? payload . retry_after
36+ : undefined ;
37+ if ( retryAfter !== undefined ) return retryAfter ;
38+ const header = response . headers . get ( "Retry-After" ) ;
39+ if ( ! header ) return undefined ;
40+ const parsed = Number ( header ) ;
41+ return Number . isFinite ( parsed ) ? parsed : undefined ;
42+ }
43+
2444function formatRetryAfterSeconds ( value : number | undefined ) : string | undefined {
2545 if ( value === undefined || ! Number . isFinite ( value ) || value < 0 ) return undefined ;
2646 const rounded = value < 10 ? value . toFixed ( 1 ) : Math . round ( value ) . toString ( ) ;
@@ -45,23 +65,60 @@ function formatDiscordApiErrorText(text: string): string | undefined {
4565 return retryAfter ? `${ message } (retry after ${ retryAfter } )` : message ;
4666}
4767
68+ export class DiscordApiError extends Error {
69+ status : number ;
70+ retryAfter ?: number ;
71+
72+ constructor ( message : string , status : number , retryAfter ?: number ) {
73+ super ( message ) ;
74+ this . status = status ;
75+ this . retryAfter = retryAfter ;
76+ }
77+ }
78+
79+ export type DiscordFetchOptions = {
80+ retry ?: RetryConfig ;
81+ label ?: string ;
82+ } ;
83+
4884export async function fetchDiscord < T > (
4985 path : string ,
5086 token : string ,
5187 fetcher : typeof fetch = fetch ,
88+ options ?: DiscordFetchOptions ,
5289) : Promise < T > {
5390 const fetchImpl = resolveFetch ( fetcher ) ;
5491 if ( ! fetchImpl ) {
5592 throw new Error ( "fetch is not available" ) ;
5693 }
57- const res = await fetchImpl ( `${ DISCORD_API_BASE } ${ path } ` , {
58- headers : { Authorization : `Bot ${ token } ` } ,
59- } ) ;
60- if ( ! res . ok ) {
61- const text = await res . text ( ) . catch ( ( ) => "" ) ;
62- const detail = formatDiscordApiErrorText ( text ) ;
63- const suffix = detail ? `: ${ detail } ` : "" ;
64- throw new Error ( `Discord API ${ path } failed (${ res . status } )${ suffix } ` ) ;
65- }
66- return ( await res . json ( ) ) as T ;
94+
95+ const retryConfig = resolveRetryConfig ( DISCORD_API_RETRY_DEFAULTS , options ?. retry ) ;
96+ return retryAsync (
97+ async ( ) => {
98+ const res = await fetchImpl ( `${ DISCORD_API_BASE } ${ path } ` , {
99+ headers : { Authorization : `Bot ${ token } ` } ,
100+ } ) ;
101+ if ( ! res . ok ) {
102+ const text = await res . text ( ) . catch ( ( ) => "" ) ;
103+ const detail = formatDiscordApiErrorText ( text ) ;
104+ const suffix = detail ? `: ${ detail } ` : "" ;
105+ const retryAfter = res . status === 429 ? parseRetryAfterSeconds ( text , res ) : undefined ;
106+ throw new DiscordApiError (
107+ `Discord API ${ path } failed (${ res . status } )${ suffix } ` ,
108+ res . status ,
109+ retryAfter ,
110+ ) ;
111+ }
112+ return ( await res . json ( ) ) as T ;
113+ } ,
114+ {
115+ ...retryConfig ,
116+ label : options ?. label ?? path ,
117+ shouldRetry : ( err ) => err instanceof DiscordApiError && err . status === 429 ,
118+ retryAfterMs : ( err ) =>
119+ err instanceof DiscordApiError && typeof err . retryAfter === "number"
120+ ? err . retryAfter * 1000
121+ : undefined ,
122+ } ,
123+ ) ;
67124}
0 commit comments