Skip to content

Commit 4a6d4c3

Browse files
authored
fix: Treat 413 HTTP status as recoverable for events. (#348)
1 parent dfffcc7 commit 4a6d4c3

File tree

5 files changed

+59
-4
lines changed

5 files changed

+59
-4
lines changed

packages/shared/common/__tests__/internal/events/EventProcessor.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -658,7 +658,9 @@ describe('given an event processor', () => {
658658
await expect(eventProcessor.flush()).rejects.toThrow('some error');
659659

660660
eventProcessor.sendEvent(new InputIdentifyEvent(Context.fromLDContext(user)));
661-
await expect(eventProcessor.flush()).rejects.toThrow(/SDK key is invalid/);
661+
await expect(eventProcessor.flush()).rejects.toThrow(
662+
'Events cannot be posted because a permanent error has been encountered.',
663+
);
662664
});
663665

664666
it('swallows errors from failed background flush', async () => {

packages/shared/common/src/errors.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,29 @@ export class LDClientError extends Error {
4343
}
4444
}
4545

46+
/**
47+
* Check if the HTTP error is recoverable. This will return false if a request
48+
* made with any payload could not recover. If the reason for the failure
49+
* is payload specific, for instance a payload that is too large, then
50+
* it could recover with a different payload.
51+
*/
4652
export function isHttpRecoverable(status: number) {
4753
if (status >= 400 && status < 500) {
4854
return status === 400 || status === 408 || status === 429;
4955
}
5056
return true;
5157
}
58+
59+
/**
60+
* Returns true if the status could recover for a different payload.
61+
*
62+
* When used with event processing this indicates that we should discard
63+
* the payload, but that a subsequent payload may succeed. Therefore we should
64+
* not stop event processing.
65+
*/
66+
export function isHttpLocallyRecoverable(status: number) {
67+
if (status === 413) {
68+
return true;
69+
}
70+
return isHttpRecoverable(status);
71+
}

packages/shared/common/src/internal/events/EventProcessor.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,11 @@ export default class EventProcessor implements LDEventProcessor {
168168

169169
async flush(): Promise<void> {
170170
if (this.shutdown) {
171-
throw new LDInvalidSDKKeyError('Events cannot be posted because SDK key is invalid');
171+
throw new LDInvalidSDKKeyError(
172+
'Events cannot be posted because a permanent error has been encountered. ' +
173+
'This is most likely an invalid SDK key. The specific error information ' +
174+
'is logged independently.',
175+
);
172176
}
173177

174178
const eventsToFlush = this.queue;

packages/shared/common/src/internal/events/EventSender.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,23 @@ describe('given an event sender', () => {
196196
});
197197
});
198198

199+
it('given a result for too large of a payload', async () => {
200+
setupMockFetch(413);
201+
eventSenderResult = await eventSender.sendEventData(
202+
LDEventType.AnalyticsEvents,
203+
testEventData1,
204+
);
205+
206+
const errorMessage = `Received error 413 for event posting - giving up permanently`;
207+
208+
const { status, error } = eventSenderResult;
209+
210+
expect(mockFetch).toHaveBeenCalledTimes(1);
211+
expect(status).toEqual(LDDeliveryStatus.Failed);
212+
expect(error.name).toEqual('LaunchDarklyUnexpectedResponseError');
213+
expect(error.message).toEqual(errorMessage);
214+
});
215+
199216
describe.each([401, 403])('given unrecoverable errors', (responseStatusCode) => {
200217
beforeEach(async () => {
201218
setupMockFetch(responseStatusCode);

packages/shared/common/src/internal/events/EventSender.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import {
55
LDEventSenderResult,
66
LDEventType,
77
} from '../../api/subsystem';
8-
import { isHttpRecoverable, LDUnexpectedResponseError } from '../../errors';
8+
import {
9+
isHttpLocallyRecoverable,
10+
isHttpRecoverable,
11+
LDUnexpectedResponseError,
12+
} from '../../errors';
913
import { ClientContext } from '../../options';
1014
import { defaultHeaders, httpErrorMessage, sleep } from '../../utils';
1115

@@ -80,7 +84,15 @@ export default class EventSender implements LDEventSender {
8084
);
8185

8286
if (!isHttpRecoverable(status)) {
83-
tryRes.status = LDDeliveryStatus.FailedAndMustShutDown;
87+
// If the HTTP request isn't recoverable. Meaning if we made the same request it
88+
// would not recover, then we check if a different request could recover.
89+
// If a different request could not recover, then we shutdown. If a different request could
90+
// recover, then we just don't retry this specific request.
91+
if (!isHttpLocallyRecoverable(status)) {
92+
tryRes.status = LDDeliveryStatus.FailedAndMustShutDown;
93+
} else {
94+
tryRes.status = LDDeliveryStatus.Failed;
95+
}
8496
tryRes.error = error;
8597
return tryRes;
8698
}

0 commit comments

Comments
 (0)