Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions etc/firebase-admin.messaging.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,11 @@ export class Messaging {
sendEachForMulticast(message: MulticastMessage, dryRun?: boolean): Promise<BatchResponse>;
sendEachForMulticast(message: FidMulticastMessage, dryRun?: boolean): Promise<BatchResponse>;
subscribeToTopic(registrationTokenOrTokens: string | string[], topic: string): Promise<MessagingTopicManagementResponse>;
// @deprecated
subscribeToTopicLegacy(registrationTokenOrTokens: string | string[], topic: string): Promise<MessagingTopicManagementResponse>;
unsubscribeFromTopic(registrationTokenOrTokens: string | string[], topic: string): Promise<MessagingTopicManagementResponse>;
// @deprecated
unsubscribeFromTopicLegacy(registrationTokenOrTokens: string | string[], topic: string): Promise<MessagingTopicManagementResponse>;
}

// @public
Expand Down
37 changes: 37 additions & 0 deletions src/messaging/messaging-api-request-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,43 @@ export class FirebaseMessagingRequestHandler {
});
}

/**
* Invokes the HTTP/2 request handler for generic operations (e.g., topic subscriptions).
*
* @param host - The host to which to send the request.
* @param path - The path to which to send the request.
* @param method - The HTTP method to use.
* @param requestData - Optional request data.
* @param http2SessionHandler - The HTTP/2 session handler.
* @returns A promise that resolves with the response data.
*/
public invokeHttp2RequestHandler(
host: string,
path: string,
method: HttpMethod,
requestData: object | undefined,
http2SessionHandler: Http2SessionHandler
): Promise<object> {
const request: Http2RequestConfig = {
method,
url: `https://${host}${path}`,
data: requestData,
headers: FIREBASE_MESSAGING_HEADERS,
timeout: FIREBASE_MESSAGING_TIMEOUT,
http2SessionHandler,
};
return this.http2Client.send(request).then((response) => {
if (!response.isJson()) {
throw new RequestResponseError(response);
}
const errorCode = getErrorCode(response.data);
if (errorCode) {
throw new RequestResponseError(response);
}
return response.data;
});
}

private buildSendResponse(response: RequestResponse): SendResponse {
const result: SendResponse = {
success: response.status === 200,
Expand Down
169 changes: 164 additions & 5 deletions src/messaging/messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { ErrorInfo } from '../utils/error';
import * as utils from '../utils';
import * as validator from '../utils/validator';
import { validateMessage } from './messaging-internal';
import { getErrorCode, createFirebaseError } from './messaging-errors-internal';
import { FirebaseMessagingRequestHandler } from './messaging-api-request-internal';

import {
Expand All @@ -36,7 +37,7 @@ import {
// Legacy API types
SendResponse,
} from './messaging-api';
import { Http2SessionHandler } from '../utils/api-request';
import { Http2SessionHandler, RequestResponseError } from '../utils/api-request';

// FCM endpoints
const FCM_SEND_HOST = 'fcm.googleapis.com';
Expand All @@ -50,9 +51,9 @@ const FCM_MAX_BATCH_SIZE = 500;
/**
* Maps a raw FCM server response to a `MessagingTopicManagementResponse` object.
*
* @param {object} response The raw FCM server response to map.
* @param response - The raw FCM server response to map.
*
* @returns {MessagingTopicManagementResponse} The mapped `MessagingTopicManagementResponse` object.
* @returns The mapped `MessagingTopicManagementResponse` object.
*/
function mapRawResponseToTopicManagementResponse(response: object): MessagingTopicManagementResponse {
// Add the success and failure counts.
Expand Down Expand Up @@ -379,7 +380,6 @@ export class Messaging {
registrationTokenOrTokens,
topic,
'subscribeToTopic',
FCM_TOPIC_MANAGEMENT_ADD_PATH,
);
}

Expand All @@ -406,6 +406,55 @@ export class Messaging {
registrationTokenOrTokens,
topic,
'unsubscribeFromTopic',
);
}

/**
* Subscribes a device or list of devices to an FCM topic using the legacy Instance ID API.
* Served as a backup/legacy endpoint.
*
* @param registrationTokenOrTokens - A token or array of registration tokens
* for the devices to subscribe to the topic.
* @param topic - The topic to which to subscribe.
*
* @returns A promise fulfilled with the server's response after the device has been
* subscribed to the topic.
*
* @deprecated Use {@link Messaging.subscribeToTopic} instead.
*/
public subscribeToTopicLegacy(
registrationTokenOrTokens: string | string[],
topic: string,
): Promise<MessagingTopicManagementResponse> {
return this.sendTopicManagementRequestLegacy(
registrationTokenOrTokens,
topic,
'subscribeToTopicLegacy',
FCM_TOPIC_MANAGEMENT_ADD_PATH,
);
}

/**
* Unsubscribes a device or list of devices from an FCM topic using the legacy Instance ID API.
* Served as a backup/legacy endpoint.
*
* @param registrationTokenOrTokens - A device registration token or an array of
* device registration tokens to unsubscribe from the topic.
* @param topic - The topic from which to unsubscribe.
*
* @returns A promise fulfilled with the server's response after the device has been
* unsubscribed from the topic.
*
* @deprecated Use {@link Messaging.unsubscribeFromTopic} instead.
*/
public unsubscribeFromTopicLegacy(
registrationTokenOrTokens: string | string[],
topic: string,
): Promise<MessagingTopicManagementResponse> {
return this.sendTopicManagementRequestLegacy(
registrationTokenOrTokens,
topic,
'unsubscribeFromTopicLegacy',
FCM_TOPIC_MANAGEMENT_REMOVE_PATH,
);
}
Expand Down Expand Up @@ -439,7 +488,6 @@ export class Messaging {
* registration tokens to unsubscribe from the topic.
* @param topic - The topic to which to subscribe.
* @param methodName - The name of the original method called.
* @param path - The endpoint path to use for the request.
*
* @returns A Promise fulfilled with the parsed server
* response.
Expand All @@ -448,6 +496,117 @@ export class Messaging {
registrationTokenOrTokens: string | string[],
topic: string,
methodName: string,
): Promise<MessagingTopicManagementResponse> {
this.validateRegistrationTokensType(registrationTokenOrTokens, methodName);
this.validateTopicType(topic, methodName);

// Prepend the topic with /topics/ if necessary.
topic = this.normalizeTopic(topic);

return Promise.resolve()
.then(() => {
// Validate the contents of the input arguments. Because we are now in a promise, any thrown
// error will cause this method to return a rejected promise.
this.validateRegistrationTokens(registrationTokenOrTokens, methodName);
this.validateTopic(topic, methodName);

// Ensure the registration token(s) input argument is an array.
let registrationTokensArray: string[] = registrationTokenOrTokens as string[];
if (validator.isString(registrationTokenOrTokens)) {
registrationTokensArray = [registrationTokenOrTokens as string];
}

return utils.findProjectId(this.app).then((projectId) => {
if (!validator.isNonEmptyString(projectId)) {
throw new FirebaseMessagingError(
messagingClientErrorCode.INVALID_ARGUMENT,
'Failed to determine project ID for Messaging. Initialize the '
+ 'SDK with service account credentials or set project ID as an app option. '
+ 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.',
);
}

const topicName = topic.replace(/^\/topics\//, '');
const isSubscribe = methodName === 'subscribeToTopic';
const httpMethod = isSubscribe ? 'POST' : 'DELETE';

const http2SessionHandler = new Http2SessionHandler('https://fcm.googleapis.com');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid hardcoding the host string. Use the FCM_SEND_HOST constant defined at the top of the file for consistency.

Suggested change
const http2SessionHandler = new Http2SessionHandler('https://fcm.googleapis.com');
const http2SessionHandler = new Http2SessionHandler(`https://${FCM_SEND_HOST}`);


const requests = registrationTokensArray.map((registrationId) => {
let requestPath = `/v1/projects/${projectId}/registrations/${registrationId}/topicSubscriptions`;
if (isSubscribe) {
requestPath += `?topic_name=${topicName}`;
} else {
requestPath += `/${topicName}?allow_missing=true`;
}
return this.messagingRequestHandler.invokeHttp2RequestHandler(
'fcm.googleapis.com',
requestPath,
httpMethod,
isSubscribe ? {} : undefined,
http2SessionHandler
);
});

return Promise.allSettled(requests).then((results) => {
if (results.length > 0 && results.every((r) => r.status === 'rejected')) {
const firstReason = (results[0] as PromiseRejectedResult).reason;
if (firstReason instanceof RequestResponseError) {
throw createFirebaseError(firstReason);
} else {
throw firstReason;
}
}

const response: MessagingTopicManagementResponse = {
successCount: 0,
failureCount: 0,
errors: [],
};

results.forEach((result, index) => {
if (result.status === 'fulfilled') {
response.successCount += 1;
} else {
response.failureCount += 1;
const err = (result as PromiseRejectedResult).reason;
const errorCode = err.response?.isJson() ? getErrorCode(err.response.data) : null;
const errorMessage = err.response?.isJson() ? err.response.data?.error?.message : err.message;
const newError = FirebaseMessagingError.fromTopicManagementServerError(
errorCode || 'UNKNOWN',
errorMessage,
err.response?.isJson() ? err.response.data : undefined
);
response.errors.push({
index,
error: newError,
});
}
});

return response;
}).finally(() => {
http2SessionHandler.close();
});
});
});
}

/**
* Helper method which sends and handles topic subscription management requests using the legacy Instance ID API.
*
* @param registrationTokenOrTokens - The registration token or an array of
* registration tokens to unsubscribe from the topic.
* @param topic - The topic to which to subscribe.
* @param methodName - The name of the original method called.
* @param path - The endpoint path to use for the request.
*
* @returns A Promise fulfilled with the parsed server response.
*/
private sendTopicManagementRequestLegacy(
registrationTokenOrTokens: string | string[],
topic: string,
methodName: string,
path: string,
): Promise<MessagingTopicManagementResponse> {
this.validateRegistrationTokensType(registrationTokenOrTokens, methodName);
Expand Down
Loading
Loading