Skip to content
Merged
Prev Previous commit
Next Next commit
Added HTTP2 support for sendEach() and sendEachForMulticast` with i…
…ntegration tests
  • Loading branch information
jonathanedey committed May 9, 2024
commit ad9d7807c9116a8454edfa8b6d5041792a9b6df3
4 changes: 2 additions & 2 deletions etc/firebase-admin.messaging.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ export class Messaging {
send(message: Message, dryRun?: boolean): Promise<string>;
// @deprecated
sendAll(messages: Message[], dryRun?: boolean): Promise<BatchResponse>;
sendEach(messages: Message[], dryRun?: boolean): Promise<BatchResponse>;
sendEachForMulticast(message: MulticastMessage, dryRun?: boolean): Promise<BatchResponse>;
sendEach(messages: Message[], dryRun?: boolean, useHttp2?: boolean): Promise<BatchResponse>;
sendEachForMulticast(message: MulticastMessage, dryRun?: boolean, useHttp2?: boolean): Promise<BatchResponse>;
// @deprecated
sendMulticast(message: MulticastMessage, dryRun?: boolean): Promise<BatchResponse>;
sendToCondition(condition: string, payload: MessagingPayload, options?: MessagingOptions): Promise<MessagingConditionResponse>;
Expand Down
40 changes: 38 additions & 2 deletions src/messaging/messaging-api-request-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { App } from '../app';
import { FirebaseApp } from '../app/firebase-app';
import {
HttpMethod, AuthorizedHttpClient, HttpRequestConfig, RequestResponseError, RequestResponse,
AuthorizedHttp2Client, Http2SessionHandler, Http2RequestConfig,
} from '../utils/api-request';
import { createFirebaseError, getErrorCode } from './messaging-errors-internal';
import { SubRequest, BatchRequestClient } from './batch-request-internal';
Expand All @@ -44,6 +45,7 @@ const LEGACY_FIREBASE_MESSAGING_HEADERS = {
*/
export class FirebaseMessagingRequestHandler {
private readonly httpClient: AuthorizedHttpClient;
private readonly http2Client: AuthorizedHttp2Client;
private readonly batchClient: BatchRequestClient;

/**
Expand All @@ -52,6 +54,7 @@ export class FirebaseMessagingRequestHandler {
*/
constructor(app: App) {
this.httpClient = new AuthorizedHttpClient(app as FirebaseApp);
this.http2Client = new AuthorizedHttp2Client(app as FirebaseApp);
this.batchClient = new BatchRequestClient(
this.httpClient, FIREBASE_MESSAGING_BATCH_URL, FIREBASE_MESSAGING_HEADERS);
}
Expand Down Expand Up @@ -97,14 +100,16 @@ export class FirebaseMessagingRequestHandler {
}

/**
* Invokes the request handler with the provided request data.
* Invokes the HTTP/1.1 request handler with the provided request data.
*
* @param host - The host to which to send the request.
* @param path - The path to which to send the request.
* @param requestData - The request data.
* @returns A promise that resolves with the {@link SendResponse}.
*/
public invokeRequestHandlerForSendResponse(host: string, path: string, requestData: object): Promise<SendResponse> {
public invokeHttpRequestHandlerForSendResponse(
host: string, path: string, requestData: object
): Promise<SendResponse> {
const request: HttpRequestConfig = {
method: FIREBASE_MESSAGING_HTTP_METHOD,
url: `https://${host}${path}`,
Expand All @@ -124,6 +129,37 @@ export class FirebaseMessagingRequestHandler {
});
}

/**
* Invokes the HTTP/2 request handler with the provided request data.
*
* @param host - The host to which to send the request.
* @param path - The path to which to send the request.
* @param requestData - The request data.
* @returns A promise that resolves with the {@link SendResponse}.
*/
public invokeHttp2RequestHandlerForSendResponse(
host: string, path: string, requestData: object, http2SessionHandler: Http2SessionHandler
): Promise<SendResponse> {
const request: Http2RequestConfig = {
method: FIREBASE_MESSAGING_HTTP_METHOD,
url: `https://${host}${path}`,
data: requestData,
headers: LEGACY_FIREBASE_MESSAGING_HEADERS,
timeout: FIREBASE_MESSAGING_TIMEOUT,
http2SessionHandler: http2SessionHandler
};
return this.http2Client.send(request).then((response) => {
return this.buildSendResponse(response);
})
.catch((err) => {
if (err instanceof RequestResponseError) {
return this.buildSendResponseFromError(err);
}
// Re-throw the error if it already has the proper format.
throw err;
});
}

/**
* Sends the given array of sub requests as a single batch to FCM, and parses the result into
* a BatchResponse object.
Expand Down
32 changes: 27 additions & 5 deletions src/messaging/messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
NotificationMessagePayload,
SendResponse,
} from './messaging-api';
import { Http2SessionHandler } from '../utils/api-request';

// FCM endpoints
const FCM_SEND_HOST = 'fcm.googleapis.com';
Expand Down Expand Up @@ -269,7 +270,7 @@ export class Messaging {
* @returns A Promise fulfilled with an object representing the result of the
* send operation.
*/
public sendEach(messages: Message[], dryRun?: boolean): Promise<BatchResponse> {
public sendEach(messages: Message[], dryRun?: boolean, useHttp2?: boolean): Promise<BatchResponse> {
if (validator.isArray(messages) && messages.constructor !== Array) {
// In more recent JS specs, an array-like object might have a constructor that is not of
// Array type. Our deepCopy() method doesn't handle them properly. Convert such objects to
Expand All @@ -291,6 +292,12 @@ export class Messaging {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT, 'dryRun must be a boolean');
}
if (typeof useHttp2 !== 'undefined' && !validator.isBoolean(useHttp2)) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT, 'enableHttp2 must be a boolean');
}

const http2SessionHandler = useHttp2 ? new Http2SessionHandler(`https://${FCM_SEND_HOST}`) : undefined

return this.getUrlPath()
.then((urlPath) => {
Expand All @@ -300,10 +307,16 @@ export class Messaging {
if (dryRun) {
request.validate_only = true;
}
return this.messagingRequestHandler.invokeRequestHandlerForSendResponse(FCM_SEND_HOST, urlPath, request);

if (http2SessionHandler){
return this.messagingRequestHandler.invokeHttp2RequestHandlerForSendResponse(
FCM_SEND_HOST, urlPath, request, http2SessionHandler);
}
return this.messagingRequestHandler.invokeHttpRequestHandlerForSendResponse(FCM_SEND_HOST, urlPath, request);
});
return Promise.allSettled(requests);
}).then((results) => {
})
.then((results) => {
const responses: SendResponse[] = [];
results.forEach(result => {
if (result.status === 'fulfilled') {
Expand All @@ -318,6 +331,11 @@ export class Messaging {
successCount,
failureCount: responses.length - successCount,
};
})
.finally(() => {
if (http2SessionHandler){
http2SessionHandler.close()
}
});
}

Expand All @@ -339,7 +357,7 @@ export class Messaging {
* @returns A Promise fulfilled with an object representing the result of the
* send operation.
*/
public sendEachForMulticast(message: MulticastMessage, dryRun?: boolean): Promise<BatchResponse> {
public sendEachForMulticast(message: MulticastMessage, dryRun?: boolean, useHttp2?: boolean): Promise<BatchResponse> {
const copy: MulticastMessage = deepCopy(message);
if (!validator.isNonNullObject(copy)) {
throw new FirebaseMessagingError(
Expand All @@ -354,6 +372,10 @@ export class Messaging {
MessagingClientErrorCode.INVALID_ARGUMENT,
`tokens list must not contain more than ${FCM_MAX_BATCH_SIZE} items`);
}
if (typeof useHttp2 !== 'undefined' && !validator.isBoolean(useHttp2)) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT, 'enableHttp2 must be a boolean');
}

const messages: Message[] = copy.tokens.map((token) => {
return {
Expand All @@ -366,7 +388,7 @@ export class Messaging {
fcmOptions: copy.fcmOptions,
};
});
return this.sendEach(messages, dryRun);
return this.sendEach(messages, dryRun, useHttp2);
}

/**
Expand Down
Loading