Skip to content

feat(fcm): Migrate topic management to FCM v1 REST API - #546

Merged
lahirumaramba merged 5 commits into
mainfrom
lm-fcm-topics
Sep 24, 2026
Merged

lahirumaramba merged 5 commits into
mainfrom
lm-fcm-topics

Conversation

@lahirumaramba

Copy link
Copy Markdown
Member

Migrates topic subscription and management from the deprecated Instance ID (IID) service to the FCM v1 REST API. The new implementation calls the FCM v1 topicSubscriptions endpoints for each registration token concurrently (bounded by a semaphore) and handles idempotent subscription scenarios (HTTP 409 / ALREADY_EXISTS). Deprecates the legacy IID topic methods.

@lahirumaramba
lahirumaramba requested a review from a team September 17, 2026 18:21
Migrates topic subscription and unsubscription from the deprecated Instance ID (IID) service to the FCM v1 REST API. The new implementation calls the FCM v1 topicSubscriptions endpoints for each registration token concurrently (bounded by a semaphore) and handles idempotent subscription scenarios (HTTP 409 / ALREADY_EXISTS). Deprecates the legacy IID topic methods.

Testing: Added unit tests in `FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs` and updated `FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs` to verify FCM v1 topic management requests, error mappings, argument validations, and legacy endpoints. All unit tests passed across target frameworks.

Context Sources Used:
- id: firebase-admin-dotnet

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces new non-legacy SubscribeToTopicAsync and UnsubscribeFromTopicAsync methods to FirebaseMessaging and FirebaseMessagingClient that utilize the new v1 FCM API, while deprecating the old Instance ID API-based methods. It also adds validation, concurrent request handling using SemaphoreSlim, updated error mapping, and comprehensive unit tests. The review feedback highlights three key areas for improvement: explicitly catching and rethrowing OperationCanceledException to prevent swallowing cancellations in the generic catch block, properly disposing of the SemaphoreSlim instance to avoid resource leaks, and using RegexOptions.Compiled on the static topic name Regex to optimize performance.

@vinclou

vinclou commented Sep 21, 2026

Copy link
Copy Markdown

I feel like our project really needs this.

Our setup is: One backend, one service account, and two Firebase projects. Sends and registrations work just fine.
I've added topic support for the public channel notifications, and the whole thing fell apart. Every topic subscription just fails. The legacy idd/v1:batchAdd request never includes the app's ProjectId, and the backend has no way to route it to the right project.


Our wrapper calls SubscribeToTopicAsync, we'd pick it up with zero code changes.

I'd rather not work around it:

  • IAM roles on the second project don't help because the legacy request never names that project. Although it is still might be the case I've updated the wrong ones?
  • A separate key per project means more creds to manage and more code changes to get around SDK routing.
  • And obviously I'd rather not reimplement this.

If there's a rough release target date, or anything else relevant to it, please do let me know.

@vinclou

vinclou commented Sep 21, 2026

Copy link
Copy Markdown

For anyone who lands here with the same setup (one service account used for several Firebase projects): sends work, but every SubscribeToTopicAsync for the other project fails with unknown-error. The legacy iid/v1:batchAdd call
doesn't include the project, so it returns PERMISSION_DENIED, and 3.6.0 reports that as unknown-error. IAM roles on the other project don't help, or I just don't know which ones would make this possible.

Thus, this PR fixes it. Topic calls become project-scoped, and topic management worked for us with no code changes.

I've run tests locally with the changes in here and a real service account my API would use.

Until it's released, you can build it yourself. This branch has the PR changes plus the bot's review
suggestions: https://github.com/vinclou/firebase-admin-dotnet/tree/pr546-review-fixes

I'm not so sure about the release schedule you have, but hopefully we all get it soon.

Thanks for the fix!

@lahirumaramba

Copy link
Copy Markdown
Member Author

Hi @vinclou, thank you for your comments and for confirming that these changes address your needs.

We are currently waiting on the remaining reviews to merge this into the main branch. Once merged, we intend to include this in the next SDK release cycle. This is a high-priority change for us, so rest assured we are actively working to move this forward.

Addresses review feedback on PR #546 by:
- Explicitly catching and rethrowing OperationCanceledException in topic management to ensure cancellation propagates properly rather than converting to UNKNOWN_ERROR.
- Disposing SemaphoreSlim inside a using block in SendTopicManagementRequestAsync to prevent resource leaks.
- Specifying RegexOptions.Compiled on the static TopicNamePattern regex to improve performance.

Testing: Added TopicManagement_Cancellation unit tests in FirebaseMessagingClientTest.cs to verify cancellation propagation and verified that all 1,088 unit tests pass.

Context Sources Used:
- id: firebase-admin-dotnet
@lahirumaramba

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request migrates topic subscription and unsubscription functionality in FirebaseMessaging from the legacy Instance ID API to the modern FCM v1 REST API, processing requests concurrently using a semaphore. It also marks the legacy methods as obsolete, updates error mapping in ErrorInfo, and adds comprehensive unit tests. The feedback suggests defining a constant for the maximum concurrent topic requests instead of using a magic number, and optimizing the subscription request body by using a static StringContent with "{}" to avoid the overhead of serializing an empty anonymous object.

private const string FcmSendUrl = FcmBaseUrl + "/v1/projects/{0}/messages:send";
private const string FcmBatchUrl = FcmBaseUrl + "/batch";

private static readonly System.Text.RegularExpressions.Regex TopicNamePattern =

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Define a constant for the maximum number of concurrent topic management requests to avoid using a magic number in the code and to improve maintainability.

        private const int MaxConcurrentTopicRequests = 100;

        private static readonly System.Text.RegularExpressions.Regex TopicNamePattern =

var cleanTopic = topic.StartsWith("/topics/") ? topic.Substring("/topics/".Length) : topic;
var encodedTopic = Uri.EscapeDataString(cleanTopic);

using (var semaphore = new SemaphoreSlim(Math.Min(registrationTokens.Count, 100)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Use the newly defined MaxConcurrentTopicRequests constant instead of the hardcoded magic number 100.

            using (var semaphore = new SemaphoreSlim(Math.Min(registrationTokens.Count, MaxConcurrentTopicRequests)))

{
Method = HttpMethod.Post,
RequestUri = new Uri(url),
Content = NewtonsoftJsonSerializer.Instance.CreateJsonHttpContent(new { }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Avoid the overhead of allocating an anonymous object and serializing it to JSON via reflection for every single token subscription request. Since the body is always an empty JSON object ({}), you can use a static StringContent directly.

                        Content = new StringContent("{}", System.Text.Encoding.UTF8, "application/json"),

@jonathanedey jonathanedey left a comment

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.

LGTM with gemini review comments!

…e subscription payload

Addresses latest PR review feedback on PR #546 by:
- Defining the `MaxConcurrentTopicRequests` constant (set to 100) instead of using a magic number for the semaphore capacity.
- Replacing reflection-based serialization of empty anonymous objects with `StringContent("{}", Encoding.UTF8, "application/json")` for subscription request bodies to reduce allocation and CPU overhead.

Testing: Ran the unit test suite (`dotnet test FirebaseAdmin/FirebaseAdmin.Tests --framework net6.0`) with all 1,088 tests passing, and verified code formatting with `dotnet format`.

Context Sources Used:
- id: firebase-admin-dotnet
@lahirumaramba lahirumaramba added the release:stage Stage a release candidate label Sep 24, 2026
…on tests

With the migration of topic management to the FCM v1 REST API (`/v1/projects/{projectId}/registrations/{token}/topicSubscriptions`), non-existent or foreign tokens like `"token1"` and `"token2"` return HTTP 403 PERMISSION_DENIED (`"permission-denied"`) from the live FCM backend rather than the legacy Instance ID API's INVALID_ARGUMENT (`"invalid-argument"`). Updated `SubscribeToTopic` and `UnsubscribeFromTopic` integration test assertions to expect `"permission-denied"`.

Testing: Built `FirebaseAdmin.IntegrationTests` across `net462` and `net6.0` with 0 warnings/errors, formatted code with `dotnet format`, and ran unit tests with all 1,088 tests passing.

Context Sources Used:
- id: firebase-admin-dotnet
@lahirumaramba
lahirumaramba merged commit f0a3238 into main Sep 24, 2026
16 checks passed
@lahirumaramba
lahirumaramba deleted the lm-fcm-topics branch September 24, 2026 16:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:stage Stage a release candidate release-note

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants