Skip to content

Add method to retrieve all users from Firebase Auth#44

Merged
avborup merged 4 commits into
mainfrom
feature/get-all-users
Sep 9, 2025
Merged

Add method to retrieve all users from Firebase Auth#44
avborup merged 4 commits into
mainfrom
feature/get-all-users

Conversation

@avborup

@avborup avborup commented Sep 4, 2025

Copy link
Copy Markdown
Owner

Adds two new methods to the Firebase Auth client:

  • get_all_users, which retrieves all users from Firebase Auth (the multi-user version of get_user)
  • get_all_users_concurrently, which retrieves the users using a specified number of workers, which can speed up the retrieval significantly for large user sets

Summary by CodeRabbit

  • New Features
    • Added ability to fetch all users from Firebase Auth, including a public convenience method for full-user retrieval and an option for concurrent retrieval.
  • Performance
    • Faster bulk user retrieval via concurrency, pagination handling, and result de-duplication.
  • Refactor
    • Centralized project-scoped endpoint handling and unified per-request authorization; improved access token handling and clearer error logging.
  • Compatibility
    • No breaking changes to the public API.

@coderabbitai

coderabbitai Bot commented Sep 4, 2025

Copy link
Copy Markdown

Walkthrough

Adds project-scoped Firebase Auth URL building, token retrieval helpers, per-request auth for GET/POST, and concurrent batch user listing (partitioned workers, pagination, HashMap dedup). Introduces a BatchGetResponse model and a single-line URL formatting tweak in the Google token endpoint. Two new public methods added.

Changes

Cohort / File(s) Summary of changes
Token URL formatting
src/auth/credential/api_auth_token.rs
Reformats the Google token endpoint URL construction into a single-line format! call; no behavioral change.
Firebase Auth client & concurrency
src/auth/mod.rs
Adds project_id + project_url helper; introduces get_access_token, auth_get, updates auth_post to add Authorization header; implements get_all_users and get_all_users_concurrently which partition work across ASCII ranges, page via accounts:batchGet, and deduplicate users via a HashMap.
Models: batch response
src/auth/models/mod.rs
Adds crate-private BatchGetResponse { users: Option<Vec<User>>, next_page_token: Option<String> } with serde(rename_all = "camelCase") for parsing batch results.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Caller as Caller
  participant FAC as FirebaseAuthClient
  participant Auth as Google OAuth
  participant API as Identity Toolkit

  Note over Caller,FAC: Caller -> get_all_users_concurrently(num_workers)

  Caller->>FAC: get_all_users_concurrently(n)
  activate FAC

  FAC->>FAC: build worker partitions (0-9A-Za-z)
  FAC->>FAC: spawn workers

  par worker loop
    loop per-page
      FAC->>FAC: get_access_token()
      FAC->>Auth: POST token endpoint (client creds)
      Auth-->>FAC: access_token
      FAC->>API: GET accounts:batchGet?maxResults&nextPageToken&filter (Authorization header)
      API-->>FAC: BatchGetResponse { users?, nextPageToken? }
      FAC->>FAC: collect users
    end
  and
  end

  FAC->>FAC: merge & deduplicate (HashMap by uid)
  FAC-->>Caller: Vec<User>
  deactivate FAC
Loading
sequenceDiagram
  autonumber
  participant FAC as FirebaseAuthClient
  participant Auth as Google OAuth

  FAC->>FAC: get_access_token()
  activate FAC
  FAC->>Auth: POST token endpoint (client creds)
  Auth-->>FAC: access_token | error
  deactivate FAC
  Note over FAC: Tokens used by auth_get/auth_post via Authorization: Bearer <token>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • JoachimBorup

Pre-merge checks (3 passed)

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly captures the pull request’s primary purpose—adding functionality to retrieve all users from Firebase Auth—and does so using clear, concise language without extraneous detail, making it easy for teammates to understand the main change at a glance.
Description Check ✅ Passed The description directly outlines the two new methods added to the Firebase Auth client, specifying their names and functionality, which aligns perfectly with the implemented changes and gives reviewers clear context for what the PR accomplishes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

Poem

Workers split the charset sea,
Tokens dance and headers flee.
Pages roll and users blend,
HashMap finds a tidy end.
Batch by batch, the list is free.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/get-all-users

Comment @coderabbitai help to get the list of available commands and usage tips.

@avborup
avborup force-pushed the feature/get-all-users branch 2 times, most recently from 0ec4a9a to 471ece4 Compare September 9, 2025 07:57
@avborup
avborup force-pushed the feature/get-all-users branch from 471ece4 to 0d674d4 Compare September 9, 2025 08:08
@avborup
avborup marked this pull request as ready for review September 9, 2025 10:07
@avborup avborup added enhancement New feature or request Auth labels Sep 9, 2025

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 688623a and 2b586c8.

📒 Files selected for processing (3)
  • src/auth/credential/api_auth_token.rs (1 hunks)
  • src/auth/mod.rs (5 hunks)
  • src/auth/models/mod.rs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/auth/credential/api_auth_token.rs (1)
src/auth/mod.rs (1)
  • url (54-56)
src/auth/mod.rs (2)
src/auth/credential/api_auth_token.rs (3)
  • new (19-25)
  • get_access_token (27-38)
  • res (73-74)
src/auth/credential/token_handler.rs (1)
  • new (18-23)
🔇 Additional comments (15)
src/auth/credential/api_auth_token.rs (1)

55-55: LGTM - URL formatting simplified.

The change from a multiline format! block to a single-line format is cleaner and more readable without changing the functionality.

src/auth/models/mod.rs (1)

14-19: LGTM - Clean model for batch response.

The BatchGetResponse struct is well-structured with proper serde attributes. The camelCase renaming and optional fields align perfectly with Firebase's API response format.

src/auth/mod.rs (13)

1-1: Good addition for deduplication.

Adding HashMap import is necessary for the deduplication logic in the concurrent user retrieval.


10-10: LGTM - Model import added.

The BatchGetResponse import is correctly added to support the new batch retrieval functionality.


31-31: LGTM - Project ID field added.

The project_id field is properly extracted from the service account and stored for use in project-scoped URLs.

Also applies to: 42-42, 50-50


58-65: LGTM - Helper method for project URLs.

The project_url helper method correctly constructs project-scoped Firebase API URLs using the stored project ID.


67-78: LGTM - Access token retrieval method.

The get_access_token method properly wraps the token manager call with error logging. The error handling preserves the original error while adding trace logging.


80-94: LGTM - Auth POST method.

The auth_post method correctly retrieves an access token and creates a request builder with the Authorization header. Good separation of concerns.


96-110: LGTM - Auth GET method.

The auth_get method mirrors the auth_post pattern perfectly, providing consistent authorization header handling for GET requests.


345-353: LGTM - Simple wrapper method.

The get_all_users method is a clean wrapper that defaults to single-threaded execution via get_all_users_concurrently(1).


355-369: Good input validation and documentation.

The method signature and docs clearly explain the worker limits and API rate limits. The clamping of num_workers is a good safety measure.


371-382: LGTM - Clean URL builder.

The make_pagination_url helper function correctly constructs pagination URLs with optional next page tokens.


384-402: Smart partitioning strategy.

The character-based partitioning using alphanumeric ranges is clever - it distributes users across workers based on UID prefixes. The range calculation handles the last chunk properly to include all remaining characters.


404-444: Solid worker implementation.

The run_worker function handles pagination correctly, processes batch responses properly, and has good boundary checking with the max parameter to prevent overlap between workers.


326-327: auth_post usage is correct; no additional direct self.client.{post,get} calls were found.

Comment thread src/auth/mod.rs
@avborup
avborup force-pushed the feature/get-all-users branch from f90202b to 84683b2 Compare September 9, 2025 10:44

@coderabbitai coderabbitai 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.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/auth/mod.rs (2)

36-39: Set a client-wide timeout.

reqwest defaults to no timeout. Add one to prevent hanging on network stalls.

         let client = reqwest::Client::builder()
             .https_only(true)
+            .timeout(std::time::Duration::from_secs(30))
             .build()
             .context("Failed to create HTTP client")?;

749-749: Typo in log format string.

There’s a stray apostrophe in the error log.

-    tracing::error!("{:?}'", &err);
+    tracing::error!("{:?}", &err);
♻️ Duplicate comments (1)
src/auth/mod.rs (1)

459-461: Nice: efficient dedup via into_values().

This addresses the prior review about avoiding the extra key Vec.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2b586c8 and 84683b2.

📒 Files selected for processing (1)
  • src/auth/mod.rs (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/auth/mod.rs (1)
src/auth/credential/api_auth_token.rs (2)
  • new (19-25)
  • get_access_token (27-38)
🔇 Additional comments (4)
src/auth/mod.rs (4)

58-65: Project-scoped URL helper looks good.

Correct path shape for v1 project-scoped endpoints.


420-427: Error message clarity.

“Failed to get all users” is fine, but echoing the response status/body is already handled by response_error. All good—just noting we’re consistent.


345-353: Wrapper method is fine.

get_all_users delegating to the concurrent variant (now sequential pager) is clean.


355-462: No changes needed; pagination semantics are correct. The Identity Toolkit API uses maxResults (up to 1000) and nextPageToken, and you must use the opaque token returned by the API—your implementation aligns with these specs.

Comment thread src/auth/mod.rs
Comment on lines +67 to +78
async fn get_access_token(&self) -> Result<String, FirebaseError> {
let access_token = self
.api_auth_token_manager
.get_access_token()
.await
.map_err(|e| {
tracing::error!("Failed to get access token: {}", e);
tracing::error!("Failed to get access token: {e}");
e
})?;

Ok(access_token)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Minor: avoid double-logging errors.

You log in map_err and again via response_error at call sites. Consider downgrading this log to debug to reduce noise.

-                tracing::error!("Failed to get access token: {e}");
+                tracing::debug!("Failed to get access token: {e}");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async fn get_access_token(&self) -> Result<String, FirebaseError> {
let access_token = self
.api_auth_token_manager
.get_access_token()
.await
.map_err(|e| {
tracing::error!("Failed to get access token: {}", e);
tracing::error!("Failed to get access token: {e}");
e
})?;
Ok(access_token)
}
async fn get_access_token(&self) -> Result<String, FirebaseError> {
let access_token = self
.api_auth_token_manager
.get_access_token()
.await
.map_err(|e| {
tracing::debug!("Failed to get access token: {e}");
e
})?;
Ok(access_token)
}
🤖 Prompt for AI Agents
In src/auth/mod.rs around lines 67 to 78, the map_err closure currently logs the
error at error level which causes duplicate noisy logs at call sites; change
that tracing::error! call to tracing::debug! (or remove the log entirely) so the
error is still mapped through unchanged but only gets logged at callers or in
debug mode, keeping the map_err closure as a pass-through that does not produce
redundant error-level logs.

Comment thread src/auth/mod.rs
Comment on lines +96 to +111
/// Creates a new `GET` request builder with the `Authorization` header set
/// to an authorized admin access token.
async fn auth_get(
&self,
url: impl AsRef<str>,
) -> Result<reqwest::RequestBuilder, FirebaseError> {
let access_token = self.get_access_token().await?;

let builder = self
.client
.get(url.as_ref())
.header("Authorization", format!("Bearer {access_token}"));

Ok(builder)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Optional: set Accept header for JSON responses.

Not required, but can make intent explicit.

-            .get(url.as_ref())
+            .get(url.as_ref())
+            .header(reqwest::header::ACCEPT, "application/json")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Creates a new `GET` request builder with the `Authorization` header set
/// to an authorized admin access token.
async fn auth_get(
&self,
url: impl AsRef<str>,
) -> Result<reqwest::RequestBuilder, FirebaseError> {
let access_token = self.get_access_token().await?;
let builder = self
.client
.get(url.as_ref())
.header("Authorization", format!("Bearer {access_token}"));
Ok(builder)
}
/// Creates a new `GET` request builder with the `Authorization` header set
/// to an authorized admin access token.
async fn auth_get(
&self,
url: impl AsRef<str>,
) -> Result<reqwest::RequestBuilder, FirebaseError> {
let access_token = self.get_access_token().await?;
let builder = self
.client
.get(url.as_ref())
.header(reqwest::header::ACCEPT, "application/json")
.header("Authorization", format!("Bearer {access_token}"));
Ok(builder)
}
🤖 Prompt for AI Agents
In src/auth/mod.rs around lines 96 to 111, the auth_get builder sets the
Authorization header but does not declare the expected response content type;
add an Accept: application/json header to the request builder so intent is
explicit. Update the chain that builds `builder` to include an
`.header("Accept", "application/json")` alongside the existing Authorization
header and return the resulting builder unchanged.

Comment thread src/auth/mod.rs
Comment on lines +355 to +364
/// Retrieve all users in Firebase Auth using `num_workers` concurrent workers. See
/// [`get_all_users`](Self::get_all_users) for more info.
///
/// The number of workers should be between 1 and 62. If you pass a number outside
/// this range, it will be clamped.
///
/// Keep in mind that, at the time of writing, the Identity Toolkit API limit is 500
/// requests/second per service account. This method will fire off multiple requests
/// simultaneously. See the up-to-date limits in the [official docs](https://firebase.google.com/docs/auth/limits).
#[tracing::instrument(name = "Get all users concurrently", skip_all)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Docs: adjust wording after dropping sharding.

The docstring still advertises concurrent workers. Update to reflect sequential paging or document why concurrency isn’t supported for listing.

🤖 Prompt for AI Agents
In src/auth/mod.rs around lines 355 to 364, the docstring still describes
concurrent workers and sharding for listing users; update it to reflect the
current sequential paging implementation (or explicitly state why concurrency
was removed/not supported). Replace references to "num_workers" and "concurrent
workers" with wording that explains the method pages sequentially through
results, mention the API rate limits and that requests are issued one page at a
time, and/or add a brief note explaining why concurrent listing isn't used
(e.g., lack of sharded endpoints or to respect per-service-account rate limits).

Comment thread src/auth/mod.rs
Comment on lines +355 to +462
/// Retrieve all users in Firebase Auth using `num_workers` concurrent workers. See
/// [`get_all_users`](Self::get_all_users) for more info.
///
/// The number of workers should be between 1 and 62. If you pass a number outside
/// this range, it will be clamped.
///
/// Keep in mind that, at the time of writing, the Identity Toolkit API limit is 500
/// requests/second per service account. This method will fire off multiple requests
/// simultaneously. See the up-to-date limits in the [official docs](https://firebase.google.com/docs/auth/limits).
#[tracing::instrument(name = "Get all users concurrently", skip_all)]
pub async fn get_all_users_concurrently(
&self,
num_workers: u8,
) -> Result<Vec<User>, FirebaseError> {
let base_url = self.project_url("/accounts:batchGet");

fn make_pagination_url(
base_url: &str,
max_results: usize,
next_page_token: Option<&str>,
) -> String {
format!(
"{base_url}?maxResults={max_results}{}",
next_page_token
.map(|token| format!("&nextPageToken={token}"))
.unwrap_or_else(|| { "".to_string() })
)
}

let legal = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
.chars()
.collect::<Vec<_>>();

let num_workers = (num_workers as usize).clamp(1, legal.len());

let chunk_size = legal.len() / num_workers;
let mut ranges = Vec::new();

for i in 0..num_workers {
let min = legal[i * chunk_size];
let max = if i < num_workers - 1 {
legal[(i + 1) * chunk_size]
} else {
(*legal.last().unwrap() as u8 + 1) as char
};

ranges.push((min, max));
}

async fn run_worker(
client: &FirebaseAuthClient,
range: (char, char),
base_url: &str,
) -> Result<Vec<User>, FirebaseError> {
const MAX_RESULTS: usize = 1000;
let (min, max) = (range.0.to_string(), range.1.to_string());

let mut all_users = Vec::new();
let mut next_page_token = Some(min);
loop {
let url = make_pagination_url(base_url, MAX_RESULTS, next_page_token.as_deref());

let res = client
.auth_get(url)
.await?
.header("Content-Type", "application/json")
.send()
.await
.context("Failed to send get all users request")?;

if !res.status().is_success() {
return Err(response_error("Failed to get all users", res).await);
}

let res_body: BatchGetResponse =
res.json().await.context("Failed to read response JSON")?;

if let Some(mut users) = res_body.users {
all_users.append(&mut users);
}

next_page_token = res_body.next_page_token.map(|t| t.to_string());

if next_page_token.is_none() || next_page_token.as_deref() >= Some(&max) {
break;
}
}

Ok(all_users)
}

let futures = ranges
.into_iter()
.map(|range| run_worker(self, range, &base_url));

let worker_results = futures::future::try_join_all(futures).await?;

let mut deduped = HashMap::new();
for users in worker_results {
for user in users {
deduped.insert(user.uid.clone(), user);
}
}

let all_users = deduped.into_values().collect();

Ok(all_users)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Concurrency approach is incorrect: nextPageToken is opaque (this will miss/duplicate users).

You're sharding by ASCII ranges and seeding nextPageToken with arbitrary chars. Per Google APIs, nextPageToken (or pageToken) is an opaque cursor returned by the server; you can't manufacture starting tokens. This will either 400, start all workers at the beginning, or silently drop/dup pages. Replace with sequential pagination.

Apply this diff to switch to a single sequential pager (keeps the public API, ignores num_workers):

@@
-    #[tracing::instrument(name = "Get all users concurrently", skip_all)]
-    pub async fn get_all_users_concurrently(
-        &self,
-        num_workers: u8,
-    ) -> Result<Vec<User>, FirebaseError> {
-        let base_url = self.project_url("/accounts:batchGet");
-
-        fn make_pagination_url(
-            base_url: &str,
-            max_results: usize,
-            next_page_token: Option<&str>,
-        ) -> String {
-            format!(
-                "{base_url}?maxResults={max_results}{}",
-                next_page_token
-                    .map(|token| format!("&nextPageToken={token}"))
-                    .unwrap_or_else(|| { "".to_string() })
-            )
-        }
-
-        let legal = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
-            .chars()
-            .collect::<Vec<_>>();
-
-        let num_workers = (num_workers as usize).clamp(1, legal.len());
-
-        let chunk_size = legal.len() / num_workers;
-        let mut ranges = Vec::new();
-
-        for i in 0..num_workers {
-            let min = legal[i * chunk_size];
-            let max = if i < num_workers - 1 {
-                legal[(i + 1) * chunk_size]
-            } else {
-                (*legal.last().unwrap() as u8 + 1) as char
-            };
-
-            ranges.push((min, max));
-        }
-
-        async fn run_worker(
-            client: &FirebaseAuthClient,
-            range: (char, char),
-            base_url: &str,
-        ) -> Result<Vec<User>, FirebaseError> {
-            const MAX_RESULTS: usize = 1000;
-            let (min, max) = (range.0.to_string(), range.1.to_string());
-
-            let mut all_users = Vec::new();
-            let mut next_page_token = Some(min);
-            loop {
-                let url = make_pagination_url(base_url, MAX_RESULTS, next_page_token.as_deref());
-
-                let res = client
-                    .auth_get(url)
-                    .await?
-                    .header("Content-Type", "application/json")
-                    .send()
-                    .await
-                    .context("Failed to send get all users request")?;
-
-                if !res.status().is_success() {
-                    return Err(response_error("Failed to get all users", res).await);
-                }
-
-                let res_body: BatchGetResponse =
-                    res.json().await.context("Failed to read response JSON")?;
-
-                if let Some(mut users) = res_body.users {
-                    all_users.append(&mut users);
-                }
-
-                next_page_token = res_body.next_page_token.map(|t| t.to_string());
-
-                if next_page_token.is_none() || next_page_token.as_deref() >= Some(&max) {
-                    break;
-                }
-            }
-
-            Ok(all_users)
-        }
-
-        let futures = ranges
-            .into_iter()
-            .map(|range| run_worker(self, range, &base_url));
-
-        let worker_results = futures::future::try_join_all(futures).await?;
-
-        let mut deduped = HashMap::new();
-        for users in worker_results {
-            for user in users {
-                deduped.insert(user.uid.clone(), user);
-            }
-        }
-
-        let all_users = deduped.into_values().collect();
-
-        Ok(all_users)
-    }
+    #[tracing::instrument(name = "Get all users concurrently", skip_all)]
+    pub async fn get_all_users_concurrently(
+        &self,
+        _num_workers: u8,
+    ) -> Result<Vec<User>, FirebaseError> {
+        let base_url = self.project_url("/accounts:batchGet");
+        const MAX_RESULTS: usize = 1000;
+
+        let mut all_users = Vec::new();
+        let mut next_page_token: Option<String> = None;
+
+        loop {
+            let mut req = self.auth_get(&base_url).await?;
+            req = req.query(&[("maxResults", MAX_RESULTS)]);
+            if let Some(ref token) = next_page_token {
+                req = req.query(&[("nextPageToken", token)]);
+            }
+
+            let res = req
+                .send()
+                .await
+                .context("Failed to send get all users request")?;
+
+            if !res.status().is_success() {
+                return Err(response_error("Failed to get all users", res).await);
+            }
+
+            let res_body: BatchGetResponse =
+                res.json().await.context("Failed to read response JSON")?;
+
+            if let Some(mut users) = res_body.users {
+                all_users.append(&mut users);
+            }
+
+            next_page_token = res_body.next_page_token;
+            if next_page_token.is_none() {
+                break;
+            }
+        }
+
+        Ok(all_users)
+    }

If you still want concurrency, you’ll need a different strategy (e.g., a single pager that feeds a bounded channel to N workers for per-user follow-up work). The listing itself can’t be sharded by token.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Retrieve all users in Firebase Auth using `num_workers` concurrent workers. See
/// [`get_all_users`](Self::get_all_users) for more info.
///
/// The number of workers should be between 1 and 62. If you pass a number outside
/// this range, it will be clamped.
///
/// Keep in mind that, at the time of writing, the Identity Toolkit API limit is 500
/// requests/second per service account. This method will fire off multiple requests
/// simultaneously. See the up-to-date limits in the [official docs](https://firebase.google.com/docs/auth/limits).
#[tracing::instrument(name = "Get all users concurrently", skip_all)]
pub async fn get_all_users_concurrently(
&self,
num_workers: u8,
) -> Result<Vec<User>, FirebaseError> {
let base_url = self.project_url("/accounts:batchGet");
fn make_pagination_url(
base_url: &str,
max_results: usize,
next_page_token: Option<&str>,
) -> String {
format!(
"{base_url}?maxResults={max_results}{}",
next_page_token
.map(|token| format!("&nextPageToken={token}"))
.unwrap_or_else(|| { "".to_string() })
)
}
let legal = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
.chars()
.collect::<Vec<_>>();
let num_workers = (num_workers as usize).clamp(1, legal.len());
let chunk_size = legal.len() / num_workers;
let mut ranges = Vec::new();
for i in 0..num_workers {
let min = legal[i * chunk_size];
let max = if i < num_workers - 1 {
legal[(i + 1) * chunk_size]
} else {
(*legal.last().unwrap() as u8 + 1) as char
};
ranges.push((min, max));
}
async fn run_worker(
client: &FirebaseAuthClient,
range: (char, char),
base_url: &str,
) -> Result<Vec<User>, FirebaseError> {
const MAX_RESULTS: usize = 1000;
let (min, max) = (range.0.to_string(), range.1.to_string());
let mut all_users = Vec::new();
let mut next_page_token = Some(min);
loop {
let url = make_pagination_url(base_url, MAX_RESULTS, next_page_token.as_deref());
let res = client
.auth_get(url)
.await?
.header("Content-Type", "application/json")
.send()
.await
.context("Failed to send get all users request")?;
if !res.status().is_success() {
return Err(response_error("Failed to get all users", res).await);
}
let res_body: BatchGetResponse =
res.json().await.context("Failed to read response JSON")?;
if let Some(mut users) = res_body.users {
all_users.append(&mut users);
}
next_page_token = res_body.next_page_token.map(|t| t.to_string());
if next_page_token.is_none() || next_page_token.as_deref() >= Some(&max) {
break;
}
}
Ok(all_users)
}
let futures = ranges
.into_iter()
.map(|range| run_worker(self, range, &base_url));
let worker_results = futures::future::try_join_all(futures).await?;
let mut deduped = HashMap::new();
for users in worker_results {
for user in users {
deduped.insert(user.uid.clone(), user);
}
}
let all_users = deduped.into_values().collect();
Ok(all_users)
}
#[tracing::instrument(name = "Get all users concurrently", skip_all)]
pub async fn get_all_users_concurrently(
&self,
_num_workers: u8,
) -> Result<Vec<User>, FirebaseError> {
let base_url = self.project_url("/accounts:batchGet");
const MAX_RESULTS: usize = 1000;
let mut all_users = Vec::new();
let mut next_page_token: Option<String> = None;
loop {
let mut req = self.auth_get(&base_url).await?;
req = req.query(&[("maxResults", MAX_RESULTS)]);
if let Some(ref token) = next_page_token {
req = req.query(&[("nextPageToken", token)]);
}
let res = req
.send()
.await
.context("Failed to send get all users request")?;
if !res.status().is_success() {
return Err(response_error("Failed to get all users", res).await);
}
let res_body: BatchGetResponse =
res.json().await.context("Failed to read response JSON")?;
if let Some(mut users) = res_body.users {
all_users.append(&mut users);
}
next_page_token = res_body.next_page_token;
if next_page_token.is_none() {
break;
}
}
Ok(all_users)
}
🤖 Prompt for AI Agents
In src/auth/mod.rs around lines 355 to 462, the current implementation shards by
ASCII ranges and seeds nextPageToken with fabricated values which is invalid
because nextPageToken is an opaque server-issued cursor and this will
miss/duplicate pages or error; replace the whole concurrent sharding logic with
a single sequential pager that ignores num_workers: call the same
/accounts:batchGet endpoint in a loop, starting with no page token, build each
page URL with the returned next_page_token until it is None, accumulate users
into a HashMap to dedupe by uid, and return the collected values; remove the
legal/ranges/chunking and run_worker logic and update the docstring to note
num_workers is ignored (or clamp it but not used), or alternatively implement a
producer-consumer where one pager feeds a bounded channel to workers if you need
concurrency for per-user processing.

Comment thread src/auth/mod.rs
Comment on lines +361 to +364
/// Keep in mind that, at the time of writing, the Identity Toolkit API limit is 500
/// requests/second per service account. This method will fire off multiple requests
/// simultaneously. See the up-to-date limits in the [official docs](https://firebase.google.com/docs/auth/limits).
#[tracing::instrument(name = "Get all users concurrently", skip_all)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Consider bounding maximum workers even if reintroducing concurrency later.

If you bring back concurrency for other flows, cap it (e.g., min(num_workers, 16)) and use a Semaphore to avoid bursts.

🤖 Prompt for AI Agents
In src/auth/mod.rs around lines 361 to 364, the comment and instrumented method
currently allow unbounded concurrent requests; when reintroducing concurrency
cap the maximum workers to avoid exceeding API limits (for example set
max_workers = min(num_workers, 16)) and protect concurrent launches with a
tokio::sync::Semaphore (acquire a permit per task and release after completion)
so bursts are limited and the request rate stays within quota.

🛠️ Refactor suggestion

Add retries/backoff and a per-request timeout to respect API limits.

You note 500 req/s, but there’s no retry/backoff or timeout. Add exponential backoff on 429/5xx and a sane timeout to avoid hung tasks.

Would you like a small helper (with jitter) wired into auth_get/auth_post?

Comment thread src/auth/mod.rs
Comment on lines +371 to +383
fn make_pagination_url(
base_url: &str,
max_results: usize,
next_page_token: Option<&str>,
) -> String {
format!(
"{base_url}?maxResults={max_results}{}",
next_page_token
.map(|token| format!("&nextPageToken={token}"))
.unwrap_or_else(|| { "".to_string() })
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Don’t hand-roll query strings; URL-encode tokens or use .query().

Manually formatting nextPageToken risks broken URLs if the token contains reserved chars. Use reqwest’s .query(...) instead.

Apply this minimal change if you keep the current structure:

-            let url = make_pagination_url(base_url, MAX_RESULTS, next_page_token.as_deref());
-            let res = client
-                .auth_get(url)
-                .await?
-                .header("Content-Type", "application/json")
-                .send()
+            let mut req = client.auth_get(base_url).await?;
+            req = req.query(&[("maxResults", MAX_RESULTS)]);
+            if let Some(ref t) = next_page_token { req = req.query(&[("nextPageToken", t)]); }
+            let res = req
+                .send()
                 .await
                 .context("Failed to send get all users request")?;

Committable suggestion skipped: line range outside the PR's diff.

Comment thread src/auth/mod.rs
let res = client
.auth_get(url)
.await?
.header("Content-Type", "application/json")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Drop Content-Type on GET.

GETs don’t have a body; setting Content-Type is misleading. If anything, set Accept: application/json.

-                    .header("Content-Type", "application/json")
+                    // Optionally: .header(reqwest::header::ACCEPT, "application/json")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.header("Content-Type", "application/json")
// Optionally: .header(reqwest::header::ACCEPT, "application/json")
🤖 Prompt for AI Agents
In src/auth/mod.rs around line 420, the request builder sets "Content-Type:
application/json" on a GET which is incorrect; remove the
.header("Content-Type", "application/json") call and, if the server expects JSON
responses, add .header("Accept", "application/json") instead so the GET is not
advertising a body content type but still expresses it expects JSON.

@avborup
avborup merged commit 5702c6c into main Sep 9, 2025
5 checks passed
@avborup
avborup deleted the feature/get-all-users branch September 9, 2025 10:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Auth enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant