Skip to content

Commit 7f45c97

Browse files
authored
Node 429 rate limit timeout support (#1085)
1 parent 0589554 commit 7f45c97

File tree

2 files changed

+97
-5
lines changed

2 files changed

+97
-5
lines changed

packages/node/src/plugins/segmentio/__tests__/publisher.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,51 @@ const testClient = new TestFetchClient()
1717
const makeReqSpy = jest.spyOn(testClient, 'makeRequest')
1818
const getLastRequest = () => makeReqSpy.mock.lastCall![0]
1919

20+
class TestHeaders implements Headers {
21+
private headers: Record<string, string>
22+
23+
constructor() {
24+
this.headers = {}
25+
}
26+
27+
append(name: string, value: string): void {
28+
if (this.headers[name]) {
29+
this.headers[name] += `, ${value}`
30+
} else {
31+
this.headers[name] = value
32+
}
33+
}
34+
35+
delete(name: string): void {
36+
delete this.headers[name]
37+
}
38+
39+
get(name: string): string | null {
40+
return this.headers[name] || null
41+
}
42+
43+
has(name: string): boolean {
44+
return name in this.headers
45+
}
46+
47+
set(name: string, value: string): void {
48+
this.headers[name] = value
49+
}
50+
51+
forEach(
52+
callback: (value: string, name: string, parent: Headers) => void
53+
): void {
54+
for (const name in this.headers) {
55+
callback(this.headers[name], name, this)
56+
}
57+
}
58+
59+
getSetCookie(): string[] {
60+
// Implement the getSetCookie method here
61+
return []
62+
}
63+
}
64+
2065
const createTestNodePlugin = (props: Partial<PublisherProps> = {}) =>
2166
createConfiguredNodePlugin(
2267
{
@@ -306,6 +351,36 @@ describe('error handling', () => {
306351
`)
307352
})
308353

354+
it('delays retrying 429 errors', async () => {
355+
jest.useRealTimers()
356+
const headers = new TestHeaders()
357+
const resetTime = Date.now() + 350
358+
headers.set('x-ratelimit-reset', resetTime.toString())
359+
makeReqSpy
360+
.mockReturnValueOnce(
361+
createError({
362+
status: 429,
363+
statusText: 'Too Many Requests',
364+
...headers,
365+
})
366+
)
367+
.mockReturnValue(createSuccess())
368+
369+
const { plugin: segmentPlugin } = createTestNodePlugin({
370+
maxRetries: 3,
371+
flushAt: 1,
372+
})
373+
374+
const context = new Context(eventFactory.alias('to', 'from'))
375+
const pendingContext = segmentPlugin.alias(context)
376+
validateMakeReqInputs(context)
377+
expect(await pendingContext).toBe(context)
378+
expect(makeReqSpy).toHaveBeenCalledTimes(2)
379+
// Check that we've waited until roughly the reset time.
380+
expect(Date.now()).toBeLessThanOrEqual(resetTime + 20)
381+
expect(Date.now()).toBeGreaterThanOrEqual(resetTime - 20)
382+
})
383+
309384
it.each([
310385
{ status: 500, statusText: 'Internal Server Error' },
311386
{ status: 300, statusText: 'Multiple Choices' },

packages/node/src/plugins/segmentio/publisher.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ export class Publisher {
215215
while (currentAttempt < maxAttempts) {
216216
currentAttempt++
217217

218+
let requestedRetryTimeout: number | undefined
218219
let failureReason: unknown
219220
try {
220221
if (this._disable) {
@@ -279,6 +280,20 @@ export class Publisher {
279280
new Error(`[${response.status}] ${response.statusText}`)
280281
)
281282
return
283+
} else if (response.status === 429) {
284+
// Rate limited, wait for the reset time
285+
if (response.headers && 'x-ratelimit-reset' in response.headers) {
286+
const rateLimitResetTimestamp = parseInt(
287+
response.headers['x-ratelimit-reset'],
288+
10
289+
)
290+
if (isFinite(rateLimitResetTimestamp)) {
291+
requestedRetryTimeout = rateLimitResetTimestamp - Date.now()
292+
}
293+
}
294+
failureReason = new Error(
295+
`[${response.status}] ${response.statusText}`
296+
)
282297
} else {
283298
// Treat other errors as transient and retry.
284299
failureReason = new Error(
@@ -298,11 +313,13 @@ export class Publisher {
298313

299314
// Retry after attempt-based backoff.
300315
await sleep(
301-
backoff({
302-
attempt: currentAttempt,
303-
minTimeout: 25,
304-
maxTimeout: 1000,
305-
})
316+
requestedRetryTimeout
317+
? requestedRetryTimeout
318+
: backoff({
319+
attempt: currentAttempt,
320+
minTimeout: 25,
321+
maxTimeout: 1000,
322+
})
306323
)
307324
}
308325
}

0 commit comments

Comments
 (0)