[GH-ISSUE #3657] createAuthClient in a Vitest fixture hangs Vitest indefinitely due to then property always available #26999

Closed
opened 2026-04-17 17:46:36 -05:00 by GiteaMirror · 5 comments
Owner

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

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Try to put better-auth client in a Vitest fixture:

import {test as baseTest} from 'vitest';
import {createAuthClient} from 'better-auth/client';

export const test = baseTest.extend<{
    authClient: ReturnType<typeof createAuthClient>
}>({
   authClient: ({}, use) => use(createAuthClient({
        baseURL: process.env.BACKEND_URL,
        basePath: '/auth'
    }))
});

Current vs. Expected behavior

Any test using the fixture hangs/timeouts never actually stepping into the test itself:

import {test} from '<the file with the fixture above>';

test('Standard sign-up is disallowed', ({authClient}) => {
});

What version of Better Auth are you using?

1.3.4

Provide environment information

Doesn't matter.

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

Client

Auth config (if applicable)

Probably doesn't matter. If needed, I can set up a reproduction repo.

Additional context

The issue is basically this: Promise.resolve(createAuthClient()) returns a never-resolving Promise instead of a resolved one because createAuthClient() returns a thenable.

  1. Vitest internally resolves a Promise with the fixture value, the value being the client in our case
  2. However, createAuthClient effectivery returns a thenable due to createProxy's get always returning something even when the path doesn't exist on the client
  3. One funny Promise behaviour happens, and instead of the client Vitest gets a new Promise which will never be resolved

I guess createAuthClient should just return undefined for any access to authClient.then.

This issue wasted my workday 🫤.

Originally created by @KazimirPodolski on GitHub (Jul 27, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3657 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce Try to put better-auth client in a [Vitest fixture](https://vitest.dev/guide/test-context.html#test-extend): ```typescript import {test as baseTest} from 'vitest'; import {createAuthClient} from 'better-auth/client'; export const test = baseTest.extend<{ authClient: ReturnType<typeof createAuthClient> }>({ authClient: ({}, use) => use(createAuthClient({ baseURL: process.env.BACKEND_URL, basePath: '/auth' })) }); ``` ### Current vs. Expected behavior Any test using the fixture hangs/timeouts never actually stepping into the test itself: ```typescript import {test} from '<the file with the fixture above>'; test('Standard sign-up is disallowed', ({authClient}) => { }); ``` ### What version of Better Auth are you using? 1.3.4 ### Provide environment information Doesn't matter. ### Which area(s) are affected? (Select all that apply) Client ### Auth config (if applicable) Probably doesn't matter. If needed, I can set up a reproduction repo. ### Additional context The issue is basically this: `Promise.resolve(createAuthClient())` returns a never-resolving Promise instead of a resolved one because **`createAuthClient()` returns a thenable**. 1. [Vitest internally resolves a Promise with the fixture value](https://github.com/vitest-dev/vitest/blob/a4df683d108369ce274052beea87b51b87c8c497/packages/runner/src/fixture.ts#L286), the value being the client in our case 2. However, `createAuthClient` effectivery returns a thenable due to [createProxy's `get`](https://github.com/better-auth/better-auth/blob/3afb25a780dc28a2f5b10a557f711d85864d22f4/packages/better-auth/src/client/proxy.ts#L41) always returning something even when the path doesn't exist on the client 3. [One funny Promise behaviour](https://medium.com/@davidcallanan/a-peculiar-promises-and-proxy-bug-that-cost-me-5-hours-javascript-3a11e1fcd713) happens, and instead of the client Vitest gets a new Promise which will never be resolved I guess `createAuthClient` should just return `undefined` for any access to `authClient.then`. <sub>This issue wasted my workday 🫤.</sub>
GiteaMirror added the lockedbug labels 2026-04-17 17:46:36 -05:00
Author
Owner

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

You nailed the root cause: the Proxy returned by createAuthClient always returns a value for any property, including 'then', so Promise.resolve (used by Vitest for fixtures) treats it as a thenable and hangs forever. This is a classic Proxy "thenable trap" in JavaScript and affects any environment that checks for thenables, not just Vitest. None of the current client implementations special-case 'then', so this happens everywhere source.

Workaround:
You can monkey-patch the Proxy handler locally to avoid this. If you control the code, you can wrap or reimplement the get trap to return undefined for 'then':

import { createAuthClient as realCreateAuthClient } from 'better-auth/client';

function createSafeAuthClient(...args) {
  const client = realCreateAuthClient(...args);
  return new Proxy(client, {
    get(target, prop, receiver) {
      if (prop === 'then') return undefined;
      return Reflect.get(target, prop, receiver);
    }
  });
}

// Use createSafeAuthClient in your fixture instead of createAuthClient

Or, if you want to patch the Proxy at the source, you can modify the get trap in createDynamicPathProxy:

get(target, prop) {
  if (prop === 'then') return undefined;
  // ...existing logic
}

This is a safe and idiomatic fix—no legitimate better-auth API uses a then property, so you won't break anything by doing this source.

Let me know if you want a more detailed patch or have questions about the workaround!

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:3124779824 --> @dosubot[bot] commented on GitHub (Jul 27, 2025): <!-- Answer --> You nailed the root cause: the Proxy returned by `createAuthClient` always returns a value for any property, including `'then'`, so Promise.resolve (used by Vitest for fixtures) treats it as a thenable and hangs forever. This is a classic Proxy "thenable trap" in JavaScript and affects any environment that checks for thenables, not just Vitest. None of the current client implementations special-case `'then'`, so this happens everywhere [`source`](https://github.com/better-auth/better-auth/blob/3afb25a780dc28a2f5b10a557f711d85864d22f4/packages/better-auth/src/client/proxy.ts). **Workaround:** You can monkey-patch the Proxy handler locally to avoid this. If you control the code, you can wrap or reimplement the get trap to return `undefined` for `'then'`: ```ts import { createAuthClient as realCreateAuthClient } from 'better-auth/client'; function createSafeAuthClient(...args) { const client = realCreateAuthClient(...args); return new Proxy(client, { get(target, prop, receiver) { if (prop === 'then') return undefined; return Reflect.get(target, prop, receiver); } }); } // Use createSafeAuthClient in your fixture instead of createAuthClient ``` Or, if you want to patch the Proxy at the source, you can modify the `get` trap in `createDynamicPathProxy`: ```js get(target, prop) { if (prop === 'then') return undefined; // ...existing logic } ``` This is a safe and idiomatic fix—no legitimate better-auth API uses a `then` property, so you won't break anything by doing this [`source`](https://github.com/better-auth/better-auth/blob/3afb25a780dc28a2f5b10a557f711d85864d22f4/packages/better-auth/src/client/proxy.ts). Let me know if you want a more detailed patch or have questions about the workaround! <!-- 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/30748b7f-99d9-461f-9494-f68d9cf07828?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/30748b7f-99d9-461f-9494-f68d9cf07828?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/30748b7f-99d9-461f-9494-f68d9cf07828?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/30748b7f-99d9-461f-9494-f68d9cf07828?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/30748b7f-99d9-461f-9494-f68d9cf07828?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/30748b7f-99d9-461f-9494-f68d9cf07828?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/30748b7f-99d9-461f-9494-f68d9cf07828?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/3657)
Author
Owner

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

Any reason why you can't export the authClient and import it in each of your tests?

<!-- gh-comment-id:3128946129 --> @ping-maxwell commented on GitHub (Jul 28, 2025): Any reason why you can't export the authClient and import it in each of your tests?
Author
Owner

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

Just ergonomics.

<!-- gh-comment-id:3128983024 --> @KazimirPodolski commented on GitHub (Jul 28, 2025): Just ergonomics.
Author
Owner

@himself65 commented on GitHub (Aug 1, 2025):

workaround should be:

import {test as baseTest} from 'vitest';
import {createAuthClient} from 'better-auth/client';

export const test = baseTest.extend<{
    auth: { client: ReturnType<typeof createAuthClient> }
}>({
   auth: ({}, use) => use({
      client: createAuthClient({
        baseURL: process.env.BACKEND_URL,
        basePath: '/auth'
    })
   })
});
<!-- gh-comment-id:3145283078 --> @himself65 commented on GitHub (Aug 1, 2025): workaround should be: ```ts import {test as baseTest} from 'vitest'; import {createAuthClient} from 'better-auth/client'; export const test = baseTest.extend<{ auth: { client: ReturnType<typeof createAuthClient> } }>({ auth: ({}, use) => use({ client: createAuthClient({ baseURL: process.env.BACKEND_URL, basePath: '/auth' }) }) }); ```
Author
Owner

@himself65 commented on GitHub (Aug 19, 2025):

Landed in e08cf6fb44

<!-- gh-comment-id:3198955736 --> @himself65 commented on GitHub (Aug 19, 2025): Landed in https://github.com/better-auth/better-auth/commit/e08cf6fb441a617c7ed3fdf01f4469f819dab9b5
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#26999