Skip to content

Commit 85abc76

Browse files
committed
Add support for legacyHttpTransport flag
1 parent 1929762 commit 85abc76

File tree

4 files changed

+139
-0
lines changed

4 files changed

+139
-0
lines changed

.github/copilot-instructions.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ This repository implements a Node.js module for sending push notifications acros
3636
### Message Building (src/utils/tools.js)
3737

3838
**buildAndroidMessage(data, options)**
39+
3940
- Converts unified notification data to Firebase Admin SDK AndroidMessage format
4041
- Returns plain JavaScript object (no wrapper functions)
4142
- Properties mapped to camelCase (Firebase SDK standard)
@@ -44,6 +45,7 @@ This repository implements a Node.js module for sending push notifications acros
4445
- Supports all 20+ AndroidNotification properties
4546

4647
**buildAndroidNotification(data)**
48+
4749
- Maps input `data` object to AndroidNotification interface
4850
- Supported properties:
4951
- Basic: `title`, `body`, `icon`, `color`, `sound`, `tag`, `imageUrl`
@@ -59,6 +61,7 @@ This repository implements a Node.js module for sending push notifications acros
5961
### FCM Configuration (src/sendFCM.js)
6062

6163
**Initialization Options:**
64+
6265
- `credential` or `serviceAccountKey` (required) - Firebase authentication
6366
- `projectId` (optional) - Explicit Google Cloud project ID
6467
- `databaseURL` (optional) - Realtime Database URL
@@ -67,6 +70,7 @@ This repository implements a Node.js module for sending push notifications acros
6770
- `databaseAuthVariableOverride` (optional) - Auth override for RTDB rules
6871
- `httpAgent` (optional) - HTTP proxy agent for network requests
6972
- `httpsAgent` (optional) - HTTPS proxy agent for network requests
73+
- `legacyHttpTransport` (optional) - Enable HTTP/1.1 transport instead of HTTP/2 (for compatibility with older Node.js or network restrictions)
7074

7175
All optional properties are dynamically added to Firebase initialization if defined.
7276

@@ -89,6 +93,7 @@ All optional properties are dynamically added to Firebase initialization if defi
8993
### Message Format
9094

9195
Firebase Admin SDK expects:
96+
9297
```javascript
9398
{
9499
tokens: [...],
@@ -134,12 +139,14 @@ Firebase Admin SDK expects:
134139
### What Changed
135140

136141
**Removed:**
142+
137143
- `buildGcmMessage()` function (wrapper pattern with toJson() method)
138144
- `buildGcmNotification()` function
139145
- Post-processing delete statements for undefined properties
140146
- References to legacy `node-gcm` library
141147

142148
**Added:**
149+
143150
- `buildAndroidMessage()` - Direct Firebase Admin SDK compatible builder
144151
- `buildAndroidNotification()` - Proper Android notification interface
145152
- 15+ new Android notification properties (ticker, sticky, visibility, LED settings, etc.)
@@ -150,12 +157,14 @@ Firebase Admin SDK expects:
150157
### Migration Pattern
151158

152159
**Before (Legacy GCM):**
160+
153161
```javascript
154162
const message = buildGcmMessage(data).toJson();
155163
// Result: wrapper object with toJson() method
156164
```
157165

158166
**After (Firebase Admin SDK):**
167+
159168
```javascript
160169
const message = buildAndroidMessage(data);
161170
// Result: plain object directly compatible with firebase-admin
@@ -164,6 +173,7 @@ const message = buildAndroidMessage(data);
164173
### Property Naming
165174

166175
All properties now use **camelCase** (Firebase Admin SDK standard):
176+
167177
- `android_channel_id``channelId`
168178
- `title_loc_key``titleLocKey`
169179
- `body_loc_key``bodyLocKey`
@@ -173,6 +183,7 @@ All properties now use **camelCase** (Firebase Admin SDK standard):
173183
### Testing
174184

175185
New test suite added with 7 test cases:
186+
176187
- `should accept projectId in settings`
177188
- `should accept databaseURL in settings`
178189
- `should accept storageBucket in settings`
@@ -186,6 +197,7 @@ All 87 tests passing, zero regressions.
186197
## Example Usage
187198

188199
See `README.md` for complete examples including:
200+
189201
- FCM settings configuration with all AppOptions
190202
- Notification data with all supported properties
191203
- Network proxy agent setup

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const settings = {
6161
serviceAccountId: '[email protected]', // Service account email (optional)
6262
httpAgent: undefined, // HTTP Agent for proxy support (optional)
6363
httpsAgent: undefined, // HTTPS Agent for proxy support (optional)
64+
legacyHttpTransport: false, // Enable HTTP/1.1 instead of HTTP/2 (optional)
6465
},
6566
apn: {
6667
token: {
@@ -476,6 +477,7 @@ The following Firebase Admin SDK `AppOptions` are supported and can be passed in
476477
- `databaseAuthVariableOverride` - Auth variable override for Realtime Database (optional)
477478
- `httpAgent` - HTTP Agent for proxy support (optional, see [Proxy](#proxy) section)
478479
- `httpsAgent` - HTTPS Agent for proxy support (optional, see [Proxy](#proxy) section)
480+
- `legacyHttpTransport` - Enable HTTP/1.1 transport instead of HTTP/2 (optional, for compatibility with older Node.js versions or network restrictions)
479481

480482
```js
481483
const tokens = ['e..Gwso:APA91.......7r910HljzGUVS_f...kbyIFk2sK6......D2s6XZWn2E21x'];

src/sendFCM.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ const sendFCM = (regIds, data, settings) => {
8989
});
9090

9191
const firebaseApp = firebaseAdmin.initializeApp(opts, appName);
92+
93+
// Enable legacy HTTP/1.1 transport if requested
94+
if (settings.fcm.legacyHttpTransport) {
95+
firebaseAdmin.messaging(firebaseApp).enableLegacyHttpTransport();
96+
}
97+
9298
firebaseAdmin.INTERNAL.appStore.removeApp(appName);
9399

94100
const promises = [];

test/send/sendFCM.js

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,4 +469,123 @@ describe('push-notifications-fcm', () => {
469469
});
470470
});
471471
});
472+
473+
describe('Legacy HTTP transport support', () => {
474+
it('should enable legacyHttpTransport when configured', (done) => {
475+
const mockEnableLegacyHttpTransport = sinon.stub();
476+
477+
// Stub messaging to track calls and return mock instance
478+
const mockMessagingStub = sinon.stub().returns({
479+
enableLegacyHttpTransport: mockEnableLegacyHttpTransport,
480+
sendEachForMulticast: () =>
481+
Promise.resolve({
482+
successCount: 1,
483+
failureCount: 0,
484+
responses: [{ error: null }],
485+
}),
486+
});
487+
488+
// Use Object.defineProperty to override the messaging getter
489+
const proto = Object.getPrototypeOf(firebaseAdmin);
490+
const propertyDescriptor = Object.getOwnPropertyDescriptor(proto, 'messaging');
491+
492+
// eslint-disable-next-line no-import-assign
493+
Object.defineProperty(firebaseAdmin, 'messaging', {
494+
value: mockMessagingStub,
495+
configurable: true,
496+
writable: true,
497+
});
498+
499+
sinon.stub(firebaseAdmin, 'initializeApp').returns({});
500+
sinon.stub(firebaseAdmin.INTERNAL.appStore, 'removeApp');
501+
502+
const fcmOptsWithLegacy = {
503+
fcm: {
504+
name: 'testAppNameLegacy',
505+
credential: { getAccessToken: () => Promise.resolve({}) },
506+
legacyHttpTransport: true,
507+
},
508+
};
509+
510+
const pnWithLegacy = new PN(fcmOptsWithLegacy);
511+
512+
pnWithLegacy
513+
.send(regIds, message)
514+
.then(() => {
515+
expect(mockEnableLegacyHttpTransport.called).to.be.true;
516+
// Restore
517+
firebaseAdmin.initializeApp.restore();
518+
firebaseAdmin.INTERNAL.appStore.removeApp.restore();
519+
// eslint-disable-next-line no-import-assign
520+
Object.defineProperty(firebaseAdmin, 'messaging', propertyDescriptor);
521+
done();
522+
})
523+
.catch((err) => {
524+
// Restore
525+
firebaseAdmin.initializeApp.restore();
526+
firebaseAdmin.INTERNAL.appStore.removeApp.restore();
527+
// eslint-disable-next-line no-import-assign
528+
Object.defineProperty(firebaseAdmin, 'messaging', propertyDescriptor);
529+
done(err);
530+
});
531+
});
532+
533+
it('should not enable legacyHttpTransport when not configured', (done) => {
534+
const mockEnableLegacyHttpTransport = sinon.stub();
535+
536+
// Stub messaging to track calls and return mock instance
537+
const mockMessagingStub = sinon.stub().returns({
538+
enableLegacyHttpTransport: mockEnableLegacyHttpTransport,
539+
sendEachForMulticast: () =>
540+
Promise.resolve({
541+
successCount: 1,
542+
failureCount: 0,
543+
responses: [{ error: null }],
544+
}),
545+
});
546+
547+
// Use Object.defineProperty to override the messaging getter
548+
const proto = Object.getPrototypeOf(firebaseAdmin);
549+
const propertyDescriptor = Object.getOwnPropertyDescriptor(proto, 'messaging');
550+
551+
// eslint-disable-next-line no-import-assign
552+
Object.defineProperty(firebaseAdmin, 'messaging', {
553+
value: mockMessagingStub,
554+
configurable: true,
555+
writable: true,
556+
});
557+
558+
sinon.stub(firebaseAdmin, 'initializeApp').returns({});
559+
sinon.stub(firebaseAdmin.INTERNAL.appStore, 'removeApp');
560+
561+
const fcmOptsWithoutLegacy = {
562+
fcm: {
563+
name: 'testAppNameNoLegacy',
564+
credential: { getAccessToken: () => Promise.resolve({}) },
565+
},
566+
};
567+
568+
const pnWithoutLegacy = new PN(fcmOptsWithoutLegacy);
569+
570+
pnWithoutLegacy
571+
.send(regIds, message)
572+
.then(() => {
573+
expect(mockEnableLegacyHttpTransport.called).to.be.false;
574+
// Restore
575+
firebaseAdmin.initializeApp.restore();
576+
firebaseAdmin.INTERNAL.appStore.removeApp.restore();
577+
// eslint-disable-next-line no-import-assign
578+
Object.defineProperty(firebaseAdmin, 'messaging', propertyDescriptor);
579+
done();
580+
})
581+
.catch((err) => {
582+
// Restore
583+
firebaseAdmin.initializeApp.restore();
584+
firebaseAdmin.INTERNAL.appStore.removeApp.restore();
585+
// eslint-disable-next-line no-import-assign
586+
Object.defineProperty(firebaseAdmin, 'messaging', propertyDescriptor);
587+
done(err);
588+
});
589+
});
590+
});
472591
});

0 commit comments

Comments
 (0)