[GH-ISSUE #3640] Bug: remaining defaults to refillAmount when null in create-api-key.ts #9669

Closed
opened 2026-04-13 05:17:06 -05:00 by GiteaMirror · 7 comments
Owner

Originally created by @eaoliver on GitHub (Jul 27, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/3640

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

There is a bug in packages/better-auth/src/plugins/api-key/routes/create-api-key.ts

In the code, the following line assigns remaining: remaining: remaining || refillAmount || null,

So setting the remaining as null to remove the cap on API usage causes it to set remaining to the refillAmount value. This creates a key usage cap when there shouldn't be.

To reproduce, create an ApiKey with the remaining set to null as follows:

const apiKeyData = await auth.api.createApiKey({
          body: {
            name: SYSTEM_TITLE_ABRV,
            expiresIn: null, // seconds or never
            userId: newUser.user.id,
            prefix: API_KEY_PREFIX,
            metadata: metadata,
            // permissions,

            // Enable rate limit throttling
            rateLimitEnabled: true,
            rateLimitTimeWindow: RATE_LIMIT_TIME_WINDOW,
            rateLimitMax: RATE_LIMIT_MAX,
            remaining: null, // There is no cap on usage.
            // Refill the API key one request per second
            refillAmount: 1,
            refillInterval: 1000,
          },
        });

This will cause remaining to be set from null to 1.

Current vs. Expected behavior

Setting remaining to null should keep it null.

What version of Better Auth are you using?

1.3.3

Provide environment information

Node 20

Which area(s) are affected? (Select all that apply)

Backend

Auth config (if applicable)

Not relevant.

Additional context

Tested on the latest version of better-auth.
The bug is on line 422 of https://github.com/better-auth/better-auth/blob/main/packages/better-auth/src/plugins/api-key/routes/create-api-key.ts

Originally created by @eaoliver on GitHub (Jul 27, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3640 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce There is a bug in packages/better-auth/src/plugins/api-key/routes/create-api-key.ts In the code, the following line assigns remaining: `remaining: remaining || refillAmount || null,` So setting the `remaining` as null to remove the cap on API usage causes it to set remaining to the refillAmount value. This creates a key usage cap when there shouldn't be. To reproduce, create an ApiKey with the `remaining` set to null as follows: ``` const apiKeyData = await auth.api.createApiKey({ body: { name: SYSTEM_TITLE_ABRV, expiresIn: null, // seconds or never userId: newUser.user.id, prefix: API_KEY_PREFIX, metadata: metadata, // permissions, // Enable rate limit throttling rateLimitEnabled: true, rateLimitTimeWindow: RATE_LIMIT_TIME_WINDOW, rateLimitMax: RATE_LIMIT_MAX, remaining: null, // There is no cap on usage. // Refill the API key one request per second refillAmount: 1, refillInterval: 1000, }, }); ``` This will cause `remaining` to be set from null to 1. ### Current vs. Expected behavior Setting `remaining` to null should keep it null. ### What version of Better Auth are you using? 1.3.3 ### Provide environment information ```bash Node 20 ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript Not relevant. ``` ### Additional context Tested on the latest version of better-auth. The bug is on line 422 of https://github.com/better-auth/better-auth/blob/main/packages/better-auth/src/plugins/api-key/routes/create-api-key.ts
GiteaMirror added the lockedbug labels 2026-04-13 05:17:06 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Jul 27, 2025):

You're right—this is a bug. The line remaining: remaining || refillAmount || null uses the logical OR operator, so if you set remaining to null, it falls through and assigns refillAmount instead. This means you can't explicitly set remaining to null to indicate "no cap"—it always gets a value if refillAmount is present, which is not the intended behavior. The schema and downstream logic treat remaining: null as "unlimited" usage, so this assignment is incorrect [source].

To fix this, change the assignment to use the nullish coalescing operator (??), which only falls back if remaining is null or undefined:

remaining: remaining ?? refillAmount ?? null,

This way, if you explicitly set remaining: null, it will stay null and not default to refillAmount.

As a workaround until this is fixed, you could avoid passing refillAmount when you want unlimited usage, but the real solution is to update the code as above.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

<!-- gh-comment-id:3123837057 --> @dosubot[bot] commented on GitHub (Jul 27, 2025): <!-- Answer --> You're right—this is a bug. The line `remaining: remaining || refillAmount || null` uses the logical OR operator, so if you set `remaining` to `null`, it falls through and assigns `refillAmount` instead. This means you can't explicitly set `remaining` to `null` to indicate "no cap"—it always gets a value if `refillAmount` is present, which is not the intended behavior. The schema and downstream logic treat `remaining: null` as "unlimited" usage, so this assignment is incorrect [[source]](https://github.com/better-auth/better-auth/blob/e7af3aa80c0cce284ed7ef3b61056322e50cd964/packages/better-auth/src/plugins/api-key/routes/create-api-key.ts). To fix this, change the assignment to use the nullish coalescing operator (`??`), which only falls back if `remaining` is `null` or `undefined`: ```ts remaining: remaining ?? refillAmount ?? null, ``` This way, if you explicitly set `remaining: null`, it will stay `null` and not default to `refillAmount`. As a workaround until this is fixed, you could avoid passing `refillAmount` when you want unlimited usage, but the real solution is to update the code as above. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/dba8a316-2c8f-43c3-8f56-a86c2ff3888b?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/dba8a316-2c8f-43c3-8f56-a86c2ff3888b?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/dba8a316-2c8f-43c3-8f56-a86c2ff3888b?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/dba8a316-2c8f-43c3-8f56-a86c2ff3888b?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/dba8a316-2c8f-43c3-8f56-a86c2ff3888b?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/dba8a316-2c8f-43c3-8f56-a86c2ff3888b?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/dba8a316-2c8f-43c3-8f56-a86c2ff3888b?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/3640)
Author
Owner

@eaoliver commented on GitHub (Jul 27, 2025):

This way, if you explicitly set remaining: null, it will stay null and not default to refillAmount.

@dosu you are incorrect again. That is not how the ?? operator works.

<!-- gh-comment-id:3124455943 --> @eaoliver commented on GitHub (Jul 27, 2025): > This way, if you explicitly set remaining: null, it will stay null and not default to refillAmount. @dosu you are incorrect again. That is not how the ?? operator works.
Author
Owner

@dosubot[bot] commented on GitHub (Jul 27, 2025):

You're right—the nullish coalescing operator (??) does not preserve an explicit null. If you use remaining: remaining ?? refillAmount ?? null, and remaining is set to null, it will still fall through and assign refillAmount—not what you want for an "unlimited" cap. The same problem exists with the logical OR (||), which also treats null (and other falsy values like 0) as a signal to fall through.

To preserve an explicit null, you need to check for it directly. For example:

remaining: remaining === null ? null : (remaining !== undefined ? remaining : (refillAmount ?? null)),

This way, if you set remaining to null, it stays null and doesn't default to refillAmount. This matches the schema and intended behavior for unlimited usage keys in the codebase (source). This pattern is important for other nullable fields as well.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

<!-- gh-comment-id:3124457502 --> @dosubot[bot] commented on GitHub (Jul 27, 2025): <!-- Answer --> You're right—the nullish coalescing operator (??) does not preserve an explicit null. If you use `remaining: remaining ?? refillAmount ?? null`, and `remaining` is set to `null`, it will still fall through and assign `refillAmount`—not what you want for an "unlimited" cap. The same problem exists with the logical OR (`||`), which also treats `null` (and other falsy values like 0) as a signal to fall through. To preserve an explicit `null`, you need to check for it directly. For example: ```ts remaining: remaining === null ? null : (remaining !== undefined ? remaining : (refillAmount ?? null)), ``` This way, if you set `remaining` to `null`, it stays `null` and doesn't default to `refillAmount`. This matches the schema and intended behavior for unlimited usage keys in the codebase ([source](https://github.com/better-auth/better-auth/blob/e7af3aa80c0cce284ed7ef3b61056322e50cd964/packages/better-auth/src/plugins/api-key/types.ts)). This pattern is important for other nullable fields as well. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/ce2e1cf7-6ce4-47f3-b6ce-88b1d880b15a?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/ce2e1cf7-6ce4-47f3-b6ce-88b1d880b15a?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/ce2e1cf7-6ce4-47f3-b6ce-88b1d880b15a?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/ce2e1cf7-6ce4-47f3-b6ce-88b1d880b15a?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/ce2e1cf7-6ce4-47f3-b6ce-88b1d880b15a?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/ce2e1cf7-6ce4-47f3-b6ce-88b1d880b15a?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/ce2e1cf7-6ce4-47f3-b6ce-88b1d880b15a?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/3640)
Author
Owner

@eaoliver commented on GitHub (Jul 27, 2025):

or better still @dosu :

remaining: remaining === null ? remaining : (remaining ?? refillAmount ?? null),

<!-- gh-comment-id:3124519112 --> @eaoliver commented on GitHub (Jul 27, 2025): or better still @dosu : ` remaining: remaining === null ? remaining : (remaining ?? refillAmount ?? null),`
Author
Owner

@dosubot[bot] commented on GitHub (Jul 27, 2025):

Yes, your proposed code—remaining: remaining === null ? remaining : (remaining ?? refillAmount ?? null),—is correct for this use case. It explicitly preserves remaining: null (for unlimited usage), while still allowing fallback to refillAmount or null only if remaining is undefined. This matches the schema and intended behavior in the codebase, where remaining: null means "no cap" and any number means "capped" [source]. This approach avoids the pitfalls of both || and ??, which would otherwise override an explicit null.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

<!-- gh-comment-id:3124519657 --> @dosubot[bot] commented on GitHub (Jul 27, 2025): <!-- Answer --> Yes, your proposed code—`remaining: remaining === null ? remaining : (remaining ?? refillAmount ?? null),`—is correct for this use case. It explicitly preserves `remaining: null` (for unlimited usage), while still allowing fallback to `refillAmount` or `null` only if `remaining` is `undefined`. This matches the schema and intended behavior in the codebase, where `remaining: null` means "no cap" and any number means "capped" [[source]](https://github.com/better-auth/better-auth/blob/main/packages/better-auth/src/plugins/api-key/routes/create-api-key.ts). This approach avoids the pitfalls of both `||` and `??`, which would otherwise override an explicit `null`. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/399c44c3-0b8b-452c-8f24-2b4a059b0bef?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/399c44c3-0b8b-452c-8f24-2b4a059b0bef?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/399c44c3-0b8b-452c-8f24-2b4a059b0bef?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/399c44c3-0b8b-452c-8f24-2b4a059b0bef?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/399c44c3-0b8b-452c-8f24-2b4a059b0bef?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/399c44c3-0b8b-452c-8f24-2b4a059b0bef?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/399c44c3-0b8b-452c-8f24-2b4a059b0bef?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/3640)
Author
Owner

@ping-maxwell commented on GitHub (Jul 28, 2025):

Setting remaining to null means it has infinite use. However in your case you're also adding a refillInterval and refillAmount, why need that if it's already infinite use?

<!-- gh-comment-id:3129048231 --> @ping-maxwell commented on GitHub (Jul 28, 2025): Setting `remaining` to null means it has infinite use. However in your case you're also adding a refillInterval and refillAmount, why need that if it's already infinite use?
Author
Owner

@eaoliver commented on GitHub (Jul 28, 2025):

My presumption is that refillAmount is the rate in which a token bucket is replenished. This is related to the other bug I posted: #3573.

There seem to be two approaches to rate limiting in better-auth: a finite number of tokens replenished per refillInterval and refillAmount on a daily? basis (like a daily quota), and a maxRequests bucket that is consumed and replenished after rateLimitTimeWindow of inactivity.

In the second case, it would be ideal if the rate limiting on API key verification was a token bucket that replenished at a certain refill rate up to a maximum bucket size.

<!-- gh-comment-id:3129240400 --> @eaoliver commented on GitHub (Jul 28, 2025): My presumption is that `refillAmount` is the rate in which a token bucket is replenished. This is related to the other bug I posted: #3573. There seem to be two approaches to rate limiting in better-auth: a finite number of tokens replenished per `refillInterval` and `refillAmount` on a _daily?_ basis (like a daily quota), and a `maxRequests` bucket that is consumed and replenished after `rateLimitTimeWindow ` of inactivity. In the second case, it would be ideal if the rate limiting on API key verification was a token bucket that replenished at a certain refill rate up to a maximum bucket size.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#9669