Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9846af3
Add FidMessage and deprecate TokenMessage
yvonnep165 May 13, 2026
97ac410
Add fids to MulticastMessage interface
yvonnep165 May 13, 2026
7bdf972
Add integration tests
yvonnep165 May 13, 2026
8b5e564
Extract API docstring
yvonnep165 May 13, 2026
208378e
Merge branch 'main' into yp-add-fid-arg
yvonnep165 May 13, 2026
96de332
Change tokens back to required field, add extra docstrings and refact…
yvonnep165 May 14, 2026
ec3fdc4
Fix the lint error
yvonnep165 May 14, 2026
6986cc7
Merge branch 'main' into yp-add-fid-arg
yvonnep165 May 14, 2026
37b2740
Merge branch 'main' into yp-add-fid-arg
yvonnep165 May 14, 2026
cf2163d
Add FidMulticastMessage and Implement Function Overloads on sendEachF…
yvonnep165 May 15, 2026
da81dc2
Extract API doc and remove hyperlink due to duplicate names from func…
yvonnep165 May 15, 2026
d914462
Update sendEachForMulticast docstring
yvonnep165 May 20, 2026
55e645e
Add token deprecation documentation
yvonnep165 May 25, 2026
2316fde
Update integration tests error code for invalid fid target
yvonnep165 May 27, 2026
7854f58
Register new installation-id-not-registered Error Code
yvonnep165 May 28, 2026
9e5d4cf
Extract api docstring
yvonnep165 May 28, 2026
7a802d9
Resolve gemini review comments and add a unit test
yvonnep165 May 28, 2026
4e584c4
Merge remote-tracking branch 'origin/main' into yp-add-fid-arg
yvonnep165 Jun 8, 2026
09f7dd7
Add explicit note in FidMulticastMessage interface to avoid confusion
yvonnep165 Jun 9, 2026
e8febe1
Merge branch 'main' into yp-add-fid-arg
yvonnep165 Jun 9, 2026
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
22 changes: 18 additions & 4 deletions etc/firebase-admin.messaging.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,16 @@ export interface FcmOptions {
analyticsLabel?: string;
}

// @public
export interface FidMessage extends BaseMessage {
fid: string;
}

// @public
export interface FidMulticastMessage extends BaseMessage {
fids: string[];
}

// Warning: (ae-forgotten-export) The symbol "FirebaseError" needs to be exported by the entry point index.d.ts
//
// @public
Expand All @@ -183,7 +193,7 @@ export interface LightSettings {
}

// @public
export type Message = TokenMessage | TopicMessage | ConditionMessage;
export type Message = FidMessage | TokenMessage | TopicMessage | ConditionMessage;

// @public
export class Messaging {
Expand All @@ -192,7 +202,9 @@ export class Messaging {
enableLegacyHttpTransport(): void;
send(message: Message, dryRun?: boolean): Promise<string>;
sendEach(messages: Message[], dryRun?: boolean): Promise<BatchResponse>;
// @deprecated
sendEachForMulticast(message: MulticastMessage, dryRun?: boolean): Promise<BatchResponse>;
sendEachForMulticast(message: FidMulticastMessage, dryRun?: boolean): Promise<BatchResponse>;
subscribeToTopic(registrationTokenOrTokens: string | string[], topic: string): Promise<MessagingTopicManagementResponse>;
unsubscribeFromTopic(registrationTokenOrTokens: string | string[], topic: string): Promise<MessagingTopicManagementResponse>;
}
Expand All @@ -207,6 +219,7 @@ export const MessagingErrorCode: {
readonly INVALID_OPTIONS: "invalid-options";
readonly INVALID_REGISTRATION_TOKEN: "invalid-registration-token";
readonly REGISTRATION_TOKEN_NOT_REGISTERED: "registration-token-not-registered";
readonly INSTALLATION_ID_NOT_REGISTERED: "installation-id-not-registered";
readonly MISMATCHED_CREDENTIAL: "mismatched-credential";
readonly INVALID_PACKAGE_NAME: "invalid-package-name";
readonly DEVICE_MESSAGE_RATE_EXCEEDED: "device-message-rate-exceeded";
Expand All @@ -232,9 +245,10 @@ export interface MessagingTopicManagementResponse {
successCount: number;
}

// @public
// @public @deprecated
export interface MulticastMessage extends BaseMessage {
// (undocumented)
fids?: string[];
// @deprecated
tokens: string[];
}

Expand All @@ -252,7 +266,7 @@ export interface SendResponse {
success: boolean;
}

// @public (undocumented)
// @public @deprecated (undocumented)
export interface TokenMessage extends BaseMessage {
// (undocumented)
token: string;
Expand Down
8 changes: 8 additions & 0 deletions src/messaging/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const MessagingErrorCode = {
INVALID_OPTIONS: 'invalid-options',
INVALID_REGISTRATION_TOKEN: 'invalid-registration-token',
REGISTRATION_TOKEN_NOT_REGISTERED: 'registration-token-not-registered',
INSTALLATION_ID_NOT_REGISTERED: 'installation-id-not-registered',
MISMATCHED_CREDENTIAL: 'mismatched-credential',
INVALID_PACKAGE_NAME: 'invalid-package-name',
DEVICE_MESSAGE_RATE_EXCEEDED: 'device-message-rate-exceeded',
Expand Down Expand Up @@ -91,6 +92,12 @@ export const messagingClientErrorCode: { readonly [K in keyof typeof MessagingEr
'error documentation for more details. Remove this registration token and stop using it to ' +
'send messages.',
},
INSTALLATION_ID_NOT_REGISTERED: {
code: MessagingErrorCode.INSTALLATION_ID_NOT_REGISTERED,
message: 'The provided installation ID is not registered. A previously valid installation ' +
'ID can be unregistered for a variety of reasons. See the error documentation for more ' +
'details. Remove this installation ID and stop using it to send messages.',
},
MISMATCHED_CREDENTIAL: {
code: MessagingErrorCode.MISMATCHED_CREDENTIAL,
message: 'The credential used to authenticate this SDK does not have permission ' +
Expand Down Expand Up @@ -202,6 +209,7 @@ const MESSAGING_SERVER_TO_CLIENT_CODE: Record<string, keyof typeof MessagingErro
THIRD_PARTY_AUTH_ERROR: 'THIRD_PARTY_AUTH_ERROR',
UNAVAILABLE: 'SERVER_UNAVAILABLE',
UNREGISTERED: 'REGISTRATION_TOKEN_NOT_REGISTERED',
UNREGISTERED_FID: 'INSTALLATION_ID_NOT_REGISTERED',
UNSPECIFIED_ERROR: 'UNKNOWN_ERROR',
};

Expand Down
2 changes: 2 additions & 0 deletions src/messaging/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export {
CriticalSound,
ConditionMessage,
FcmOptions,
FidMessage,
FidMulticastMessage,
LightSettings,
Message,
MessagingTopicManagementResponse,
Expand Down
49 changes: 44 additions & 5 deletions src/messaging/messaging-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ export interface BaseMessage {
fcmOptions?: FcmOptions;
}

/**
* Interface representing a message that targets a Firebase Installation ID (FID).
*/
export interface FidMessage extends BaseMessage {
/**
* The Firebase Installation ID (FID) to which the message should be sent.
*/
fid: string;
}
Comment thread
yvonnep165 marked this conversation as resolved.

/**
* @deprecated Use {@link FidMessage} instead.
*/
export interface TokenMessage extends BaseMessage {
token: string;
}
Expand All @@ -40,18 +53,44 @@ export interface ConditionMessage extends BaseMessage {

/**
* Payload for the {@link Messaging.send} operation. The payload contains all the fields
* in the BaseMessage type, and exactly one of token, topic or condition.
* in the BaseMessage type, and exactly one of fid, token (deprecated, use fid instead),
* topic or condition.
*/
export type Message = TokenMessage | TopicMessage | ConditionMessage;
export type Message = FidMessage | TokenMessage | TopicMessage | ConditionMessage;

/**
* Payload for the {@link Messaging.sendEachForMulticast} method. The payload contains all the fields
* in the BaseMessage type, and a list of tokens.
* Payload for the `sendEachForMulticast` method.
*
* @deprecated Use {@link FidMulticastMessage} instead.
*/
export interface MulticastMessage extends BaseMessage {
/**
* A list of Firebase Installation IDs (FIDs) to target.
*/
fids?: string[];
/**
* A list of registration tokens to target.
*
* @deprecated Use `fids` in {@link FidMulticastMessage} instead.
*/
tokens: string[];
}

/**
* Payload for the `sendEachForMulticast` method containing only FIDs.
*
* @remarks
* Note: This is a temporary interface. In the next major version, this will be
* renamed back to `MulticastMessage` once the old token-based `MulticastMessage`
* is fully deprecated and removed.
*/
export interface FidMulticastMessage extends BaseMessage {
/**
* A list of Firebase Installation IDs (FIDs) to target.
*/
fids: string[];
}
Comment thread
yvonnep165 marked this conversation as resolved.

/**
* A notification that can be included in {@link Message}.
*/
Expand Down Expand Up @@ -698,7 +737,7 @@ export interface MessagingTopicManagementResponse {

/**
* Interface representing the server response from the
* {@link Messaging.sendEach} and {@link Messaging.sendEachForMulticast} methods.
* {@link Messaging.sendEach} and `sendEachForMulticast` methods.
*/
export interface BatchResponse {

Expand Down
17 changes: 16 additions & 1 deletion src/messaging/messaging-errors-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,23 @@ export function createFirebaseError(err: RequestResponseError): FirebaseMessagin
if (err.response.isJson()) {
// For JSON responses, map the server response to a client-side error.
const json = err.response.data;
const errorCode = getErrorCode(json);
let errorCode = getErrorCode(json);
const errorMessage = getErrorMessage(json);
if (errorCode === 'UNREGISTERED' || errorCode === 'NOT_FOUND') {
let requestData = err.response.config && err.response.config.data;
if (requestData && (typeof requestData === 'string' || Buffer.isBuffer(requestData))) {
try {
const strData = typeof requestData === 'string' ? requestData : requestData.toString('utf-8');
requestData = JSON.parse(strData);
} catch (e) {
// Ignore parsing errors.
}
}
Comment thread
yvonnep165 marked this conversation as resolved.
const messageObj = requestData && requestData.message;
if (messageObj && typeof messageObj.fid === 'string') {
errorCode = 'UNREGISTERED_FID';
}
}
return FirebaseMessagingError.fromServerError(errorCode, errorMessage, err);
}

Expand Down
4 changes: 2 additions & 2 deletions src/messaging/messaging-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ export function validateMessage(message: Message): void {
}
}

const targets = [anyMessage.token, anyMessage.topic, anyMessage.condition];
const targets = [anyMessage.fid, anyMessage.token, anyMessage.topic, anyMessage.condition];
if (targets.filter((v) => validator.isNonEmptyString(v)).length !== 1) {
throw new FirebaseMessagingError(
messagingClientErrorCode.INVALID_PAYLOAD,
'Exactly one of topic, token or condition is required');
'Exactly one of fid, topic, token or condition is required');
}

validateStringMap(message.data, 'data');
Expand Down
81 changes: 58 additions & 23 deletions src/messaging/messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { FirebaseMessagingRequestHandler } from './messaging-api-request-interna

import {
BatchResponse,
FidMulticastMessage,
Message,
MessagingTopicManagementResponse,
MulticastMessage,
Expand Down Expand Up @@ -274,50 +275,84 @@ export class Messaging {
}

/**
* Sends the given multicast message to all the FCM registration tokens
* Sends the given multicast message to all the FCM registration tokens or fids
* specified in it.
*
* This method uses the {@link Messaging.sendEach} API under the hood to send the given
* message to all the target recipients. The responses list obtained from the
* return value corresponds to the order of tokens in the `MulticastMessage`.
* return value corresponds to the order of tokens/fids in the `MulticastMessage`.

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.

nit: Maybe clarify that tokens take precedence if both are provided.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ok, added!

* If both `tokens` and `fids` are provided, `tokens` are processed first, followed by `fids`.
* An error from this method or a `BatchResponse` with all failures indicates a total
* failure, meaning that the messages in the list could be sent. Partial failures or
* failures are only indicated by a `BatchResponse` return value.
* failure, meaning that the messages in the list could not be sent. Partial failures
* are only indicated by a `BatchResponse` return value.
*
* @param message - A multicast message
* containing up to 500 tokens.
* @deprecated Use the overload accepting {@link FidMulticastMessage} instead.
*
* @param message - A multicast message containing up to 500 tokens and/or fids.
* @param dryRun - Whether to send the message in the dry-run
* (validation only) mode.
* @returns A Promise fulfilled with an object representing the result of the
* send operation.
*/
public sendEachForMulticast(message: MulticastMessage, dryRun?: boolean): Promise<BatchResponse> {
const copy: MulticastMessage = deepCopy(message);
public sendEachForMulticast(message: MulticastMessage, dryRun?: boolean): Promise<BatchResponse>;

/**
* Sends the given multicast message to all the FCM fids specified in it.
*
* This method uses the {@link Messaging.sendEach} API under the hood to send the given
* message to all the target recipients. The responses list obtained from the
* return value corresponds to the order of fids in the `FidMulticastMessage`.
* An error from this method or a `BatchResponse` with all failures indicates a total
* failure, meaning that the messages in the list could not be sent. Partial failures
* are only indicated by a `BatchResponse` return value.
*
* @param message - A multicast message containing up to 500 fids.
* @param dryRun - Whether to send the message in the dry-run (validation only) mode.
* @returns A Promise fulfilled with an object representing the result of the send operation.
*/
public sendEachForMulticast(message: FidMulticastMessage, dryRun?: boolean): Promise<BatchResponse>;

public sendEachForMulticast(
message: MulticastMessage | FidMulticastMessage,
dryRun?: boolean,
): Promise<BatchResponse> {
const copy: any = deepCopy(message);
if (!validator.isNonNullObject(copy)) {
throw new FirebaseMessagingError(
messagingClientErrorCode.INVALID_ARGUMENT, 'MulticastMessage must be a non-null object');
}
if (!validator.isNonEmptyArray(copy.tokens)) {

const { tokens, fids, ...baseMessage } = copy;

if (tokens !== undefined && !validator.isArray(tokens)) {
throw new FirebaseMessagingError(
messagingClientErrorCode.INVALID_ARGUMENT, 'tokens must be a valid array');
}
if (fids !== undefined && !validator.isArray(fids)) {
throw new FirebaseMessagingError(
messagingClientErrorCode.INVALID_ARGUMENT, 'fids must be a valid array');
}
Comment thread
yvonnep165 marked this conversation as resolved.

const tokenList: string[] = tokens || [];
const fidList: string[] = fids || [];

if (tokenList.length === 0 && fidList.length === 0) {
throw new FirebaseMessagingError(
messagingClientErrorCode.INVALID_ARGUMENT, 'tokens must be a non-empty array');
messagingClientErrorCode.INVALID_ARGUMENT, 'Either tokens or fids must be a non-empty array');
}
if (copy.tokens.length > FCM_MAX_BATCH_SIZE) {

const totalLength = tokenList.length + fidList.length;
if (totalLength > FCM_MAX_BATCH_SIZE) {
throw new FirebaseMessagingError(
messagingClientErrorCode.INVALID_ARGUMENT,
`tokens list must not contain more than ${FCM_MAX_BATCH_SIZE} items`);
`The total number of tokens and fids must not exceed ${FCM_MAX_BATCH_SIZE}.`);
}

const messages: Message[] = copy.tokens.map((token) => {
return {
token,
android: copy.android,
apns: copy.apns,
data: copy.data,
notification: copy.notification,
webpush: copy.webpush,
fcmOptions: copy.fcmOptions,
};
});
const messages: Message[] = [
...tokenList.map((token) => ({ ...baseMessage, token } as Message)),
...fidList.map((fid) => ({ ...baseMessage, fid } as Message)),
];

return this.sendEach(messages, dryRun);
}

Expand Down
6 changes: 6 additions & 0 deletions src/utils/api-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ export interface RequestResponse {
readonly data?: any;
/** For multipart responses, the payloads of individual parts. */
readonly multipart?: Buffer[];
/** Optional request config used to send this request. */
readonly config?: any;
/**
* Indicates if the response content is JSON-formatted or not. If true, data field can be used
* to retrieve the content as a parsed JSON object.
Expand Down Expand Up @@ -129,6 +131,7 @@ class DefaultRequestResponse implements RequestResponse {
public readonly status: number;
public readonly headers: any;
public readonly text?: string;
public readonly config?: RequestConfig;

private readonly parsedData: any;
private readonly parseError: Error;
Expand All @@ -140,6 +143,7 @@ class DefaultRequestResponse implements RequestResponse {
this.status = resp.status;
this.headers = resp.headers;
this.text = resp.data;
this.config = resp.config;
try {
if (!resp.data) {
throw new FirebaseAppError({ code: AppErrorCode.INTERNAL_ERROR, message: 'HTTP response missing data.' });
Expand Down Expand Up @@ -177,11 +181,13 @@ class MultipartRequestResponse implements RequestResponse {
public readonly status: number;
public readonly headers: any;
public readonly multipart?: Buffer[];
public readonly config?: any;

constructor(resp: LowLevelResponse) {
this.status = resp.status;
this.headers = resp.headers;
this.multipart = resp.multipart;
this.config = resp.config;
}

get text(): string {
Expand Down
Loading
Loading