Skip to content

Commit df90602

Browse files
authored
feat: limit retries to 5xx and 429 (#500)
* feat: limit PutRumEvents retry to 5xx * feat: add 429 * cleanup * cleanup * docs * doc * fix: fmt
1 parent 59904c8 commit df90602

File tree

3 files changed

+51
-12
lines changed

3 files changed

+51
-12
lines changed

src/dispatch/RetryHttpHandler.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { HttpHandler, HttpRequest, HttpResponse } from '@aws-sdk/protocol-http';
2+
import { is2xx, is429, is5xx } from '../plugins/utils/http-utils';
23

34
export type BackoffFunction = (retry: number) => number;
45

@@ -30,14 +31,21 @@ export class RetryHttpHandler implements HttpHandler {
3031
while (true) {
3132
try {
3233
const response = await this.handler.handle(request);
33-
if (this.isStatusCode2xx(response.response.statusCode)) {
34+
if (is2xx(response.response.statusCode)) {
3435
return response;
3536
}
36-
throw new Error(`${response.response.statusCode}`);
37+
throw response.response.statusCode;
3738
} catch (e) {
38-
if (!retriesLeft) {
39+
if (typeof e === 'number' && !is429(e) && !is5xx(e)) {
40+
// Fail immediately on client errors because they will never succeed.
41+
// Only retry when request is throttled (429) or received server error (5xx).
42+
throw new Error(`${e}`);
43+
}
44+
45+
if (retriesLeft <= 0) {
3946
throw e;
4047
}
48+
4149
retriesLeft--;
4250
await this.sleep(this.backoff(this.retries - retriesLeft));
4351
}
@@ -49,8 +57,4 @@ export class RetryHttpHandler implements HttpHandler {
4957
setTimeout(resolve, milliseconds)
5058
);
5159
}
52-
53-
private isStatusCode2xx = (statusCode: number): boolean => {
54-
return statusCode >= 200 && statusCode < 300;
55-
};
5660
}

src/dispatch/__tests__/RetryHttpHandler.test.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { RetryHttpHandler } from '../RetryHttpHandler';
66
import { FetchHttpHandler } from '@aws-sdk/fetch-http-handler';
77

88
const fetchHandler = jest.fn<Promise<Record<string, unknown>>, []>(() =>
9-
Promise.resolve({})
9+
Promise.resolve({ response: { statusCode: 500 } })
1010
);
1111
jest.mock('@aws-sdk/fetch-http-handler', () => ({
1212
FetchHttpHandler: jest
@@ -93,9 +93,9 @@ describe('RetryHttpHandler tests', () => {
9393
expect(response).resolves.toBe(success);
9494
});
9595

96-
test('when status code is not 2xx then request fails', async () => {
96+
test('when status code is 4xx then request fails without retrying', async () => {
9797
// Init
98-
const success = { response: { statusCode: 500 } };
98+
const success = { response: { statusCode: 400 } };
9999
fetchHandler.mockReturnValue(Promise.resolve(success));
100100

101101
const retries = 0;
@@ -118,14 +118,15 @@ describe('RetryHttpHandler tests', () => {
118118
await client.sendFetch(Utils.PUT_RUM_EVENTS_REQUEST);
119119
} catch (e) {
120120
// Assert
121-
expect(e.message).toEqual('500');
121+
expect(e.message).toEqual('400');
122122
return;
123123
}
124+
expect(fetchHandler).toHaveBeenCalledTimes(1);
124125

125126
fail('Request should fail');
126127
});
127128

128-
test('when status code is not 2xx then request retries', async () => {
129+
test('when status code is 5xx then request retries', async () => {
129130
// Init
130131
const badStatus = { response: { statusCode: 500 } };
131132
const okStatus = { response: { statusCode: 200 } };
@@ -157,6 +158,38 @@ describe('RetryHttpHandler tests', () => {
157158
expect(response).resolves.toBe(okStatus);
158159
});
159160

161+
test('when status code is 429 then request retries', async () => {
162+
// Init
163+
const badStatus = { response: { statusCode: 429 } };
164+
const okStatus = { response: { statusCode: 200 } };
165+
fetchHandler
166+
.mockReturnValueOnce(Promise.resolve(badStatus))
167+
.mockReturnValue(Promise.resolve(okStatus));
168+
169+
const retries = 1;
170+
const retryHandler = new RetryHttpHandler(
171+
new FetchHttpHandler(),
172+
retries,
173+
mockBackoff
174+
);
175+
176+
const client: DataPlaneClient = new DataPlaneClient({
177+
fetchRequestHandler: retryHandler,
178+
beaconRequestHandler: undefined,
179+
endpoint: Utils.AWS_RUM_ENDPOINT,
180+
region: Utils.AWS_RUM_REGION,
181+
credentials: Utils.createAwsCredentials()
182+
});
183+
184+
// Run
185+
const response: Promise<{ response: HttpResponse }> = client.sendFetch(
186+
Utils.PUT_RUM_EVENTS_REQUEST
187+
);
188+
189+
// Assert
190+
expect(response).resolves.toBe(okStatus);
191+
});
192+
160193
test('when request fails then retry succeeds after exponential backoff', async () => {
161194
// Init
162195
jest.useFakeTimers({ legacyFakeTimers: true });

src/plugins/utils/http-utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ export const defaultConfig: HttpPluginConfig = {
5959
addXRayTraceIdHeader: false
6060
};
6161

62+
export const is2xx = (status: number) => 200 <= status && status < 300;
63+
6264
export const is4xx = (status: number) => {
6365
return Math.floor(status / 100) === 4;
6466
};

0 commit comments

Comments
 (0)