Skip to content

Commit 3bef73d

Browse files
committed
Add Discord rate limit logging
1 parent 92e206b commit 3bef73d

4 files changed

Lines changed: 123 additions & 2 deletions

File tree

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import * as dotenv from 'dotenv'
1616
import { setupClientCommands } from 'setupCommands'
1717
import { serve } from '@hono/node-server'
1818
import { preloadBackgrounds } from './utils/backgroundManager'
19+
import { attachDiscordRateLimitLogging } from './utils/discordRateLimitLogger'
1920

2021
dotenv.config()
2122

@@ -33,6 +34,7 @@ process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {
3334
})
3435

3536
setupClientCommands(client)
37+
attachDiscordRateLimitLogging(client.rest, 'bot')
3638

3739
const token = process.env.DISCORD_TOKEN || ''
3840

src/setupCommands.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fs from 'node:fs'
33
import path from 'node:path'
44
import { REST, Routes } from 'discord.js'
55
import * as dotenv from 'dotenv'
6+
import { attachDiscordRateLimitLogging } from './utils/discordRateLimitLogger'
67
require('dotenv').config()
78
dotenv.config()
89

@@ -44,6 +45,7 @@ export function setupClientCommands(client: Client, deploy: boolean = false) {
4445
if (deploy) {
4546
// Construct and prepare an instance of the REST module
4647
const rest = new REST().setToken(token)
48+
attachDiscordRateLimitLogging(rest, 'command-deploy')
4749

4850
// and deploy your commands!
4951
;(async () => {
@@ -67,4 +69,4 @@ export function setupClientCommands(client: Client, deploy: boolean = false) {
6769
}
6870
})()
6971
}
70-
}
72+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import {
2+
APIRequest,
3+
InvalidRequestWarningData,
4+
REST,
5+
RESTEvents,
6+
RateLimitData,
7+
ResponseLike,
8+
} from '@discordjs/rest'
9+
10+
let discordRestTraceSeq = 0
11+
12+
function nextTraceId(source: string, kind: string): string {
13+
discordRestTraceSeq += 1
14+
return `${source}:${kind}:${discordRestTraceSeq}`
15+
}
16+
17+
function baseTrace(source: string, kind: string) {
18+
return {
19+
traceId: nextTraceId(source, kind),
20+
source,
21+
loggedAt: new Date().toISOString(),
22+
uptimeSec: Math.round(process.uptime()),
23+
}
24+
}
25+
26+
function getHeader(response: ResponseLike, name: string): string | null {
27+
return response.headers.get(name)
28+
}
29+
30+
function formatRequest(request: APIRequest) {
31+
return {
32+
method: request.method,
33+
route: request.route,
34+
path: request.path,
35+
retries: request.retries,
36+
auth: request.data.auth ?? true,
37+
hasBody: request.data.body != null,
38+
fileCount: request.data.files?.length ?? 0,
39+
}
40+
}
41+
42+
function format429Headers(response: ResponseLike) {
43+
return {
44+
retryAfter: getHeader(response, 'retry-after'),
45+
scope: getHeader(response, 'x-ratelimit-scope'),
46+
bucket: getHeader(response, 'x-ratelimit-bucket'),
47+
limit: getHeader(response, 'x-ratelimit-limit'),
48+
remaining: getHeader(response, 'x-ratelimit-remaining'),
49+
reset: getHeader(response, 'x-ratelimit-reset'),
50+
resetAfter: getHeader(response, 'x-ratelimit-reset-after'),
51+
global: getHeader(response, 'x-ratelimit-global'),
52+
via: getHeader(response, 'via'),
53+
cfRay: getHeader(response, 'cf-ray'),
54+
}
55+
}
56+
57+
function logRateLimited(source: string, data: RateLimitData): void {
58+
console.warn('[DISCORD RATE LIMIT]', {
59+
...baseTrace(source, 'rateLimited'),
60+
global: data.global,
61+
scope: data.scope,
62+
method: data.method,
63+
route: data.route,
64+
url: data.url,
65+
hash: data.hash,
66+
majorParameter: data.majorParameter,
67+
limit: data.limit,
68+
retryAfterMs: data.retryAfter,
69+
timeToResetMs: data.timeToReset,
70+
sublimitTimeoutMs: data.sublimitTimeout,
71+
})
72+
}
73+
74+
function logInvalidRequestWarning(
75+
source: string,
76+
data: InvalidRequestWarningData,
77+
): void {
78+
console.warn('[DISCORD INVALID REQUEST WARNING]', {
79+
...baseTrace(source, 'invalidRequestWarning'),
80+
count: data.count,
81+
remainingTimeMs: data.remainingTime,
82+
})
83+
}
84+
85+
function log429Response(
86+
source: string,
87+
request: APIRequest,
88+
response: ResponseLike,
89+
): void {
90+
console.error('[DISCORD 429 RESPONSE]', {
91+
...baseTrace(source, '429'),
92+
request: formatRequest(request),
93+
response: {
94+
status: response.status,
95+
statusText: response.statusText,
96+
headers: format429Headers(response),
97+
},
98+
})
99+
}
100+
101+
export function attachDiscordRateLimitLogging(
102+
rest: REST,
103+
source: string,
104+
): void {
105+
rest.on(RESTEvents.RateLimited, (data) => {
106+
logRateLimited(source, data)
107+
})
108+
109+
rest.on(RESTEvents.InvalidRequestWarning, (data) => {
110+
logInvalidRequestWarning(source, data)
111+
})
112+
113+
rest.on(RESTEvents.Response, (request, response) => {
114+
if (response.status !== 429) return
115+
log429Response(source, request, response)
116+
})
117+
}

src/utils/matchHelpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1530,7 +1530,7 @@ export async function updateMatchCountChannel(): Promise<void> {
15301530
)
15311531
.catch((err) => {
15321532
if (err.code !== 50013) {
1533-
// Only log if it's not a rate limit error
1533+
// Ignore missing permission noise here. REST rate limits are logged globally.
15341534
console.log('Failed to update match count channel:', err.message)
15351535
}
15361536
})

0 commit comments

Comments
 (0)