Skip to content

Commit 33eb7d6

Browse files
fix: retry transient write failures after receiving goaway
1 parent 1db632d commit 33eb7d6

File tree

2 files changed

+23
-15
lines changed

2 files changed

+23
-15
lines changed

lib/client.js

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,27 @@ module.exports = function (dependencies) {
138138
return subDirectoryObject;
139139
};
140140

141+
/**
142+
* Determines if a request should be retried based on the error. This includes certain status codes, expired provider token, and transient write failures.
143+
* @param {Object} error - An object representing the error which may include the following properties:
144+
* @param {number} [error.status] - The HTTP status code returned from the APNs server.
145+
* @param {VError} error.error - The error details which may include a message describing the error.
146+
* @param {string} [error.error.message] - The error message which may indicate specific conditions such as 'ExpiredProviderToken' or transient write failures.
147+
* @returns {boolean} - Returns true if the request is considered retryable based on the error, otherwise false.
148+
*/
149+
function isRequestRetryable(error) {
150+
const isStatusCodeRetryable = [408, 429, 500, 502, 503, 504].includes(error.status);
151+
152+
const isProviderTokenExpired =
153+
error.status === 403 && error.error?.message === 'ExpiredProviderToken';
154+
155+
// This can happen after the server initiates a goaway, and the client did not finish closing and destroying the session yet, so the client tries to send a request on a closing session.
156+
const isTransientWriteFailure = !!error.error?.message?.startsWith('apn write failed');
157+
158+
return isStatusCodeRetryable || isProviderTokenExpired || isTransientWriteFailure;
159+
}
160+
141161
Client.prototype.write = async function write(notification, subDirectory, type, method, count) {
142-
const retryStatusCodes = [408, 429, 500, 502, 503, 504];
143162
const retryCount = count || 0;
144163
const subDirectoryLabel = this.subDirectoryLabel(type) ?? type;
145164
const subDirectoryInformation = this.makeSubDirectoryTypeObject(
@@ -198,13 +217,7 @@ module.exports = function (dependencies) {
198217
);
199218
return { ...subDirectoryInformation, ...sentRequest };
200219
} catch (error) {
201-
// Determine if this is a retryable request.
202-
if (
203-
retryStatusCodes.includes(error.status) ||
204-
(typeof error.error !== 'undefined' &&
205-
error.status == 403 &&
206-
error.error.message === 'ExpiredProviderToken')
207-
) {
220+
if (isRequestRetryable(error)) {
208221
try {
209222
const resentRequest = await this.retryRequest(
210223
error,
@@ -255,13 +268,7 @@ module.exports = function (dependencies) {
255268
);
256269
return { ...subDirectoryInformation, ...sentRequest };
257270
} catch (error) {
258-
// Determine if this is a retryable request.
259-
if (
260-
retryStatusCodes.includes(error.status) ||
261-
(typeof error.error !== 'undefined' &&
262-
error.status == 403 &&
263-
error.error.message === 'ExpiredProviderToken')
264-
) {
271+
if (isRequestRetryable(error)) {
265272
try {
266273
const resentRequest = await this.retryRequest(
267274
error,

test/.jshintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"esversion": 11,
23
"expr": true,
34
"strict": false,
45
"mocha": true,

0 commit comments

Comments
 (0)