Skip to content

Commit 894bee1

Browse files
committed
Add support for legacyHttpTransport flag
1 parent d0e4fb3 commit 894bee1

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

0 commit comments

Comments
 (0)