Skip to content

Commit

Permalink
support 201 and 204 status codes
Browse files Browse the repository at this point in the history
  • Loading branch information
cbaker6 committed Jan 16, 2025
1 parent 209794a commit 1ac618e
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 14 deletions.
45 changes: 37 additions & 8 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,24 @@ module.exports = function (dependencies) {
return this.manageChannelsSessionPromise;
};

Client.prototype.createHeaderObject = function createHeaderObject(
uniqueId,
requestId,
channelId
) {
const header = {};
if (uniqueId) {
header['apns-unique-id'] = uniqueId;
}
if (requestId) {
header['apns-request-id'] = requestId;
}
if (channelId) {
header['apns-channel-id'] = channelId;
}
return header;
};

Client.prototype.request = async function request(
session,
address,
Expand All @@ -517,6 +535,9 @@ module.exports = function (dependencies) {
let tokenGeneration = null;
let status = null;
let retryAfter = null;
let uniqueId = null;
let requestId = null;
let channelId = null;
let responseData = '';

const headers = extend(
Expand Down Expand Up @@ -544,30 +565,38 @@ module.exports = function (dependencies) {
request.on('response', headers => {
status = headers[HTTP2_HEADER_STATUS];
retryAfter = headers['Retry-After'];
uniqueId = headers['apns-unique-id'];
requestId = headers['apns-request-id'];
channelId = headers['apns-channel-id'];
});

request.on('data', data => {
responseData += data;
});

request.write(notification.body);
if (Object.keys(notification.body).length > 0) {
request.write(notification.body);
}

return new Promise((resolve, reject) => {
request.on('end', () => {
try {
if (this.logger.enabled) {
this.logger(`Request ended with status ${status} and responseData: ${responseData}`);
}
const headerObject = this.createHeaderObject(uniqueId, requestId, channelId);

if (status === 200) {
resolve();
if (status === 200 || status === 201 || status === 204) {
const body = responseData !== '' ? JSON.parse(responseData) : {};
resolve({ ...headerObject, ...body });
return;
} else if ([TIMEOUT_STATUS, ABORTED_STATUS, ERROR_STATUS].includes(status)) {
const error = {
status,
retryAfter,
error: new VError('Timeout, aborted, or other unknown error'),
};
reject(error);
reject({ ...headerObject, ...error });
return;
} else if (responseData !== '') {
const response = JSON.parse(responseData);
Expand All @@ -579,23 +608,23 @@ module.exports = function (dependencies) {
retryAfter,
error: new VError(response.reason),
};
reject(error);
reject({ ...headerObject, ...error });

Check warning on line 611 in lib/client.js

View check run for this annotation

Codecov / codecov/patch

lib/client.js#L611

Added line #L611 was not covered by tests
return;
} else if (status === 500 && response.reason === 'InternalServerError') {
const error = {
status,
retryAfter,
error: new VError('Error 500, stream ended unexpectedly'),
};
reject(error);
reject({ ...headerObject, ...error });
return;
}
reject({ status, retryAfter, response });
reject({ ...headerObject, status, retryAfter, response });
} else {
const error = {
error: new VError(`stream ended unexpectedly with status ${status} and empty body`),
};
reject(error);
reject({ ...headerObject, ...error });
}
} catch (e) {
const error = new VError(e, 'Unexpected error processing APNs response');
Expand Down
141 changes: 135 additions & 6 deletions test/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const VError = require('verror');
const net = require('net');
const http2 = require('http2');

const { HTTP2_METHOD_POST } = http2.constants;
const { HTTP2_METHOD_POST, HTTP2_METHOD_GET, HTTP2_METHOD_DELETE } = http2.constants;

const debug = require('debug')('apn');
const credentials = require('../lib/credentials')({
Expand Down Expand Up @@ -1662,12 +1662,21 @@ describe('ManageChannelsClient', () => {
expect(requestsServed).to.equal(6);
});

it('Treats HTTP 200 responses as successful for allChannels', async () => {
it('Treats HTTP 201 responses as successful for channels', async () => {
let didRequest = false;
let establishedConnections = 0;
let requestsServed = 0;
const method = HTTP2_METHOD_POST;
const path = PATH_ALL_CHANNELS;
const path = PATH_CHANNELS;
const channel = 'dHN0LXNyY2gtY2hubA==';
const requestId = '0309F412-AA57-46A8-9AC6-B5AECA8C4594';
const uniqueId = '4C106C5F-2013-40B9-8193-EAA270B8F2C5';
const additionalHeaderInfo = {
'apns-channel-id': channel,
'apns-request-id': requestId,
'apns-unique-id': uniqueId,
};

server = createAndStartMockServer(TEST_PORT, (req, res, requestBody) => {
expect(req.headers).to.deep.equal({
':authority': '127.0.0.1',
Expand All @@ -1679,7 +1688,7 @@ describe('ManageChannelsClient', () => {
expect(requestBody).to.equal(MOCK_BODY);
// res.setHeader('X-Foo', 'bar');
// res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.writeHead(200);
res.writeHead(201, additionalHeaderInfo);
res.end('');
requestsServed += 1;
didRequest = true;
Expand All @@ -1696,8 +1705,128 @@ describe('ManageChannelsClient', () => {
body: MOCK_BODY,
};
const bundleId = BUNDLE_ID;
const result = await client.write(mockNotification, bundleId, 'allChannels', 'post');
expect(result).to.deep.equal({ bundleId });
const result = await client.write(mockNotification, bundleId, 'channels', 'post');
expect(result).to.deep.equal({ ...additionalHeaderInfo, bundleId });
expect(didRequest).to.be.true;
};
expect(establishedConnections).to.equal(0); // should not establish a connection until it's needed
// Validate that when multiple valid requests arrive concurrently,
// only one HTTP/2 connection gets established
await Promise.all([
runSuccessfulRequest(),
runSuccessfulRequest(),
runSuccessfulRequest(),
runSuccessfulRequest(),
runSuccessfulRequest(),
]);
didRequest = false;
await runSuccessfulRequest();
expect(establishedConnections).to.equal(1); // should establish a connection to the server and reuse it
expect(requestsServed).to.equal(6);
});

it('Treats HTTP 204 responses as successful for channels', async () => {
let didRequest = false;
let establishedConnections = 0;
let requestsServed = 0;
const method = HTTP2_METHOD_DELETE;
const path = PATH_CHANNELS;
const channel = 'dHN0LXNyY2gtY2hubA==';
const requestId = '0309F412-AA57-46A8-9AC6-B5AECA8C4594';
const additionalHeaderInfo = { 'apns-request-id': requestId };

server = createAndStartMockServer(TEST_PORT, (req, res, requestBody) => {
expect(req.headers).to.deep.equal({
':authority': '127.0.0.1',
':method': method,
':path': path,
':scheme': 'https',
'apns-channel-id': channel,
...additionalHeaderInfo,
});
expect(requestBody).to.be.empty;
// res.setHeader('X-Foo', 'bar');
// res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.writeHead(204, additionalHeaderInfo);
res.end('');
requestsServed += 1;
didRequest = true;
});
server.on('connection', () => (establishedConnections += 1));
await new Promise(resolve => server.on('listening', resolve));

client = createClient(CLIENT_TEST_PORT);

const runSuccessfulRequest = async () => {
const mockHeaders = { 'apns-channel-id': channel, ...additionalHeaderInfo };
const mockNotification = {
headers: mockHeaders,
body: {},
};
const bundleId = BUNDLE_ID;
const result = await client.write(mockNotification, bundleId, 'channels', 'delete');
expect(result).to.deep.equal({ ...additionalHeaderInfo, bundleId });
expect(didRequest).to.be.true;
};
expect(establishedConnections).to.equal(0); // should not establish a connection until it's needed
// Validate that when multiple valid requests arrive concurrently,
// only one HTTP/2 connection gets established
await Promise.all([
runSuccessfulRequest(),
runSuccessfulRequest(),
runSuccessfulRequest(),
runSuccessfulRequest(),
runSuccessfulRequest(),
]);
didRequest = false;
await runSuccessfulRequest();
expect(establishedConnections).to.equal(1); // should establish a connection to the server and reuse it
expect(requestsServed).to.equal(6);
});

it('Treats HTTP 200 responses as successful for allChannels', async () => {
let didRequest = false;
let establishedConnections = 0;
let requestsServed = 0;
const method = HTTP2_METHOD_GET;
const path = PATH_ALL_CHANNELS;
const channels = { channels: ['dHN0LXNyY2gtY2hubA=='] };
const requestId = '0309F412-AA57-46A8-9AC6-B5AECA8C4594';
const uniqueId = '4C106C5F-2013-40B9-8193-EAA270B8F2C5';
const additionalHeaderInfo = { 'apns-request-id': requestId, 'apns-unique-id': uniqueId };

server = createAndStartMockServer(TEST_PORT, (req, res, requestBody) => {
expect(req.headers).to.deep.equal({
':authority': '127.0.0.1',
':method': method,
':path': path,
':scheme': 'https',
'apns-request-id': requestId,
});

expect(requestBody).to.be.empty;

const data = JSON.stringify(channels);
res.writeHead(200, additionalHeaderInfo);
res.write(data);
res.end();
requestsServed += 1;
didRequest = true;
});
server.on('connection', () => (establishedConnections += 1));
await new Promise(resolve => server.on('listening', resolve));

client = createClient(CLIENT_TEST_PORT);

const runSuccessfulRequest = async () => {
const mockHeaders = { 'apns-request-id': requestId };
const mockNotification = {
headers: mockHeaders,
body: {},
};
const bundleId = BUNDLE_ID;
const result = await client.write(mockNotification, bundleId, 'allChannels', 'get');
expect(result).to.deep.equal({ ...additionalHeaderInfo, bundleId, ...channels });
expect(didRequest).to.be.true;
};
expect(establishedConnections).to.equal(0); // should not establish a connection until it's needed
Expand Down

0 comments on commit 1ac618e

Please sign in to comment.