Skip to content

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

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

lahirumaramba merged 5 commits into
mainfrom
lm-fcm-topics

Conversation

@lahirumaramba

Copy link
Copy Markdown
Member

Migrates subscribe_to_topic and unsubscribe_from_topic in the messaging module from the legacy Instance ID (IID) API to the FCM v1 Topic Subscriptions API.

Key changes:

  • Updated subscribe_to_topic and unsubscribe_from_topic to call the FCM v1 endpoints (/v1/projects/{projectId}/registrations/{token}/topicSubscriptions).
  • Added asynchronous counterparts subscribe_to_topic_async and unsubscribe_from_topic_async utilizing HttpxAsyncClient (HTTP/2).
  • Added deprecated legacy methods subscribe_to_topic_legacy and unsubscribe_from_topic_legacy with deprecation warnings.
  • Bounded client-side concurrency to 100 simultaneous requests.
  • Handled idempotency: ALREADY_EXISTS (HTTP 409) is treated as success for topic subscriptions, while NOT_FOUND (HTTP 404) is recorded as a failure for topic unsubscriptions.

@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 the topic management functionality in the Firebase Cloud Messaging module from the legacy Instance ID (IID) API to the FCM v1 API, introducing new synchronous and asynchronous methods (subscribe_to_topic, subscribe_to_topic_async, unsubscribe_from_topic, and unsubscribe_from_topic_async) while deprecating the legacy versions. The feedback highlights a potential concurrency race condition in the multi-threaded request execution where a shared headers dictionary is mutated, and suggests extracting duplicated error-handling logic into a single helper method to improve maintainability.

Comment thread firebase_admin/messaging.py
Comment thread firebase_admin/messaging.py Outdated
@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 the topic subscription and unsubscription features to the FCM v1 API, introducing both synchronous and asynchronous implementations (subscribe_to_topic, subscribe_to_topic_async, unsubscribe_from_topic, and unsubscribe_from_topic_async). The previous Instance ID API implementations have been renamed to subscribe_to_topic_legacy and unsubscribe_from_topic_legacy and marked as deprecated. Comprehensive unit tests have been added to cover the new functionality, validation rules, and error handling. There are no review comments to address, and I have no additional feedback to provide.

@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 one comment on error parsing which isn't blocking.


return 'UNKNOWN_ERROR'

def _build_topic_subscription_result(self, response, is_subscribe):

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.

Not blocking but is there a reason we don't reuse the _build_fcm_error and _build_fcm_error_httpx methods to parse the responses here. I think I follow that there needs to be a pre-check based on is_subscribe. Is there anything else?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think it was because we had to map the error codes to keep backward compatibility with the legacy subscription methods. I will add a task to clean this up in the next major version. Thanks!

@yvonnep165 yvonnep165 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!

…ic error parsing

- Pass a copy of self._fcm_headers in topic management requests to prevent concurrent mutation race conditions in ThreadPoolExecutor.
- Extract common error parsing logic from _build_topic_subscription_result_from_requests_error and _build_topic_subscription_result_from_httpx_error into _build_topic_subscription_result.
- Hoist URL-encoded topic computation out of the per-token request loops.
- Mount an HTTPAdapter with a connection pool size of 100 on the FCM client session.
- Fix line length and method signature override lint warnings in test_messaging.py.
- Added type annotations to public topic management functions and internal methods
- Updated HTTP status code mapping (401->UNAUTHENTICATED, 503->UNAVAILABLE, 408/504->DEADLINE_EXCEEDED) and prioritized status codes over free-form message strings
- Updated topic regex with \Z to reject trailing newlines
- Passed header copies in send, send_each, and send_each_async to prevent concurrency mutations
- Added test coverage for status code mapping, async argument validation, prefixed topics, and async batch unsubscribe
@lahirumaramba lahirumaramba added the release:stage Stage a release candidate label Sep 23, 2026
@lahirumaramba
lahirumaramba merged commit 46edf94 into main Sep 23, 2026
24 checks passed
@lahirumaramba
lahirumaramba deleted the lm-fcm-topics branch September 23, 2026 15:35
akalex added a commit to healthjoy/async-firebase that referenced this pull request Oct 4, 2026
* feat(messaging): Migrate topic management to FCM v1 API

subscribe_devices_to_topic() and unsubscribe_devices_from_topic() now
call the FCM v1 topic subscriptions endpoint instead of the Instance ID
API, following firebase/firebase-admin-python#980. One request is sent
per device token, at most 100 in flight (or max_connections if lower).

Request-level failures (401, timeouts, connection errors) are now
reported per token in TopicManagementResponse.errors instead of
TopicManagementResponse.exception. Subscribing an already subscribed
token (ALREADY_EXISTS / CONFLICT) counts as success; 409 ABORTED does
not. Arguments are validated: up to 1000 non-empty device tokens and a
well-formed topic name, optionally prefixed with /topics/.

The previous IID behavior is kept as the deprecated
subscribe_devices_to_topic_legacy() and
unsubscribe_devices_from_topic_legacy().

Bump version to 6.3.0.

* fix(messaging): Address review findings on FCM v1 topic management

- Prepare headers once per call instead of once per device token. A
  failed access token fetch is now reported for every token without
  sending requests, instead of being retried serially by each task.
- Share the concurrency limit across concurrent calls on a client.
- Escape the device token in the request path, so a "/" in a token can
  no longer change the path.
- Tolerate non-dict JSON error bodies and malformed "details" when
  resolving error reasons, instead of raising AttributeError and
  aborting the whole batch.

* fix: Raise on OAuth token errors and deduplicate topic device tokens

- CredentialManager.get_access_token() now calls raise_for_status(), so
  an error response from the token endpoint (e.g. a revoked key) is
  handled as an HTTP error instead of escaping as KeyError('expires_in').
  send() and send_each() return an FCMResponse with exception set, and
  topic management reports the error for every device token.
- Topic management sends one request per unique device token and maps
  each outcome back to every index where the token appears, avoiding
  concurrent requests for the same subscription.

* fix(messaging): Use a plain string error field as the topic error reason

Bodies like {"error": "invalid_grant"} from the OAuth token endpoint, or
{"error": "INVALID_REGISTRATION"} from legacy-style APIs, now report the
string as the per-token reason instead of the generic HTTP status code.
The send() error path is unchanged.

* refactor(messaging): Make FCM v1 topic management backward compatible

Restore subscribe_devices_to_topic() and unsubscribe_devices_from_topic()
to the Instance ID implementation, unchanged apart from a
DeprecationWarning, and expose the FCM v1 implementation as the new
subscribe_to_topic() and unsubscribe_from_topic(). Remove the *_legacy
methods.

Revert the changes to shared code paths so existing behavior matches
6.2.2: CredentialManager no longer calls raise_for_status(), and
_parse_platform_error(), _get_fcm_error_type() and the IID response
parser are back to their original code. The v1 error handler now has
its own lenient body parsing.

* refactor(messaging): Keep topic management helpers out of the public API

- Make TOPIC_PREFIX, TOPIC_NAME_PATTERN and
  TopicManagementResponse.from_error_reasons() private, so 6.3.0 adds
  only the public names callers need.
- Reword the subscribe_to_topic()/unsubscribe_from_topic() docstrings
  and README so they don't imply a rejected access token is reported
  per token.
- Move the v1 error helpers above the public API section in responses,
  fix the public function count, and note why the FCM error code lookup
  is not shared with _get_fcm_error_type().

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(messaging): Make the topic concurrency cap private and pin legacy behavior

- Rename TOPIC_MANAGEMENT_MAX_CONCURRENCY to
  _TOPIC_MANAGEMENT_MAX_CONCURRENCY; it is an internal tuning value.
- List the remaining public additions (TOPIC_MANAGEMENT_MAX_DEVICE_TOKENS
  and AsyncClientBase.FCM_REGISTRATIONS_ENDPOINT) in CHANGES.
- Add a test that the deprecated IID methods still send input the new
  v1 methods reject, guarding backward compatibility.

* fix(messaging): Harden FCM v1 topic management per review

- Decide "already subscribed" from the HTTP status (409) or a status of
  ALREADY_EXISTS/CONFLICT, before resolving the reason, matching the
  official Firebase Admin SDKs (Python, Node, Go, Java, .NET). An FCM
  errorCode in the body can no longer override it, and 409 ABORTED now
  counts as already subscribed, as in those SDKs.
- Use only non-empty string error codes, so a malformed error body
  (e.g. a list status) falls back to the HTTP status instead of raising
  TypeError and aborting the call.
- Create the concurrency semaphore per call. A semaphore cached on the
  client bound to the first event loop and raised RuntimeError when the
  client was reused from another loop.
- Stop logging the request URL, which contains the device token.
- Return the HTTP error from _send_topic_subscription_request and let
  each per-token step apply its own rule.
- Clarify in CHANGES that the deprecated methods now emit a warning.

* refactor(messaging): Address Codacy findings in topic management

- Build TopicManagementResponse from per-token outcomes in a private
  client function that sets only public fields, instead of calling the
  private TopicManagementResponse._from_error_reasons() from another
  module. messages.py is back to its 6.2.2 code.
- Use the fake_device_token fixture in the logging test and check both
  the raw and URL-encoded token, replacing the hardcoded token literal
  that Bandit reported as B105.

* test: Add FCM v1 topic management integration tests

Exercise subscribe_to_topic()/unsubscribe_from_topic() against the real
FCM project with a made-up registration token. FCM may accept or reject
it, so the tests assert that each request is routed, authorized and
answered with a Google API error or success, and that the public
methods report exactly one outcome per token. This catches a wrong
endpoint, HTTP method or missing permission, but cannot cover the
success or "already subscribed" paths for a real device.

Move the shared credential check into tests/integration.py and the
fcm_client fixture into conftest.py so both integration modules use
them.

* ci: Rename the integration test step

The step runs every test marked integration, which now includes the
topic management tests, not only FCM payload validation.
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