Skip to content
Draft
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
142 changes: 142 additions & 0 deletions src/messaging/messaging-api-request-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
import { createFirebaseError, getErrorCode } from './messaging-errors-internal';
import { getSdkVersion } from '../utils/index';
import { SendResponse } from './messaging-api';
import * as validator from '../utils/validator';
import { FirebaseMessagingError } from './error';

export interface TopicSubscriptionResponse {
success: boolean;
error?: FirebaseMessagingError;
}


// FCM backend constants
Expand Down Expand Up @@ -170,4 +177,139 @@
error: createFirebaseError(err)
};
}

/**
* Invokes the HTTP/1.1 request handler for a topic management operation.
*
* @param host - The host to which to send the request.
* @param path - The path to which to send the request.
* @param methodName - The name of the calling method (subscribeToTopic or unsubscribeFromTopic).
* @param requestData - The request data (or undefined for DELETE).
* @returns A promise that resolves with the {@link TopicSubscriptionResponse}.
*/
public invokeHttpRequestHandlerForTopicSubscriptionResponse(
host: string, path: string, methodName: string, requestData?: object
): Promise<TopicSubscriptionResponse> {
const request: HttpRequestConfig = {
method: methodName === 'subscribeToTopic' ? 'POST' : 'DELETE',
url: `https://${host}${path}`,
data: requestData,
headers: FIREBASE_MESSAGING_HEADERS,
timeout: FIREBASE_MESSAGING_TIMEOUT,
};
return this.httpClient.send(request).then((response) => {
return this.buildTopicSubscriptionResponse(response, methodName);
})
.catch((err) => {
if (err instanceof RequestResponseError) {
return this.buildTopicSubscriptionResponseFromError(err, methodName);
}
// Re-throw the error if it already has the proper format.
throw err;
});
}

/**
* Invokes the HTTP/2 request handler for a topic management operation.
*
* @param host - The host to which to send the request.
* @param path - The path to which to send the request.
* @param methodName - The name of the calling method (subscribeToTopic or unsubscribeFromTopic).
* @param requestData - The request data (or undefined for DELETE).
* @param http2SessionHandler - The HTTP/2 session handler.
* @returns A promise that resolves with the {@link TopicSubscriptionResponse}.
*/
public invokeHttp2RequestHandlerForTopicSubscriptionResponse(
host: string, path: string, methodName: string, requestData: object | undefined, http2SessionHandler: Http2SessionHandler

Check failure on line 223 in src/messaging/messaging-api-request-internal.ts

View workflow job for this annotation

GitHub Actions / build (24.x)

This line has a length of 125. Maximum allowed is 120

Check failure on line 223 in src/messaging/messaging-api-request-internal.ts

View workflow job for this annotation

GitHub Actions / build (26.x)

This line has a length of 125. Maximum allowed is 120
): Promise<TopicSubscriptionResponse> {
const request: Http2RequestConfig = {
method: methodName === 'subscribeToTopic' ? 'POST' : 'DELETE',
url: `https://${host}${path}`,
data: requestData,
headers: FIREBASE_MESSAGING_HEADERS,
timeout: FIREBASE_MESSAGING_TIMEOUT,
http2SessionHandler: http2SessionHandler
};
return this.http2Client.send(request).then((response) => {
return this.buildTopicSubscriptionResponse(response, methodName);
})
.catch((err) => {
if (err instanceof RequestResponseError) {
return this.buildTopicSubscriptionResponseFromError(err, methodName);
}
// Re-throw the error if it already has the proper format.
throw err;
});
}

private buildTopicSubscriptionResponse(
response: RequestResponse, methodName: string
): TopicSubscriptionResponse {
if (response.status === 200) {
return { success: true };
}
return this.buildTopicSubscriptionResponseFromError(new RequestResponseError(response), methodName);
}

private buildTopicSubscriptionResponseFromError(
err: RequestResponseError, methodName: string
): TopicSubscriptionResponse {
if (err.response.isJson()) {
const json = err.response.data;
const errorCode = getErrorCode(json);
if (methodName === 'subscribeToTopic' && (errorCode === 'ALREADY_EXISTS' || err.response.status === 409)) {
return { success: true };
}
const errorMessage = (validator.isNonNullObject(json) && 'error' in json && validator.isNonEmptyString((json as any).error.message))

Check failure on line 263 in src/messaging/messaging-api-request-internal.ts

View workflow job for this annotation

GitHub Actions / build (24.x)

This line has a length of 138. Maximum allowed is 120

Check failure on line 263 in src/messaging/messaging-api-request-internal.ts

View workflow job for this annotation

GitHub Actions / build (26.x)

This line has a length of 138. Maximum allowed is 120
? (json as any).error.message
: undefined;
Comment on lines +263 to +265

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.

high

If the server returns a JSON response where the error field is explicitly null (e.g., { "error": null }), evaluating (json as any).error.message will throw a TypeError: Cannot read properties of null (reading 'message') and crash the process. Adding a check to ensure json.error is a non-null object before accessing its properties prevents this potential runtime exception.

Suggested change
const errorMessage = (validator.isNonNullObject(json) && 'error' in json && validator.isNonEmptyString((json as any).error.message))
? (json as any).error.message
: undefined;
const errorMessage = (validator.isNonNullObject(json) && 'error' in json && validator.isNonNullObject((json as any).error) && validator.isNonEmptyString((json as any).error.message))
? (json as any).error.message
: undefined;

return {
success: false,
error: FirebaseMessagingError.fromTopicManagementServerError(
errorCode || 'UNKNOWN_ERROR',
errorMessage,
err,
),
};
}

// Non-JSON response
if (methodName === 'subscribeToTopic' && err.response.status === 409) {
return { success: true };
}

let serverErrorCode = 'UNKNOWN_ERROR';

Check failure on line 281 in src/messaging/messaging-api-request-internal.ts

View workflow job for this annotation

GitHub Actions / build (24.x)

The value assigned to 'serverErrorCode' is not used in subsequent statements

Check failure on line 281 in src/messaging/messaging-api-request-internal.ts

View workflow job for this annotation

GitHub Actions / build (26.x)

The value assigned to 'serverErrorCode' is not used in subsequent statements
switch (err.response.status) {
case 400:
serverErrorCode = 'INVALID_ARGUMENT';
break;
case 401:
case 403:
serverErrorCode = 'PERMISSION_DENIED';
break;
case 404:
serverErrorCode = 'NOT_FOUND';
break;
case 429:
serverErrorCode = 'RESOURCE_EXHAUSTED';
break;
case 500:
serverErrorCode = 'INTERNAL';
break;
case 503:
serverErrorCode = 'DEADLINE_EXCEEDED';
break;
default:
serverErrorCode = 'UNKNOWN_ERROR';
}

return {
success: false,
error: FirebaseMessagingError.fromTopicManagementServerError(
serverErrorCode,
`Server responded with status ${err.response.status}.`,
err,
),
};
}
}
Loading
Loading