[GH-ISSUE #3650] Dynamic Import of createAuthClient Breaks with Proxy: Triggers Unexpected .then Call and 404 #18304

Closed
opened 2026-04-15 16:43:52 -05:00 by GiteaMirror · 3 comments
Owner

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

Hi,

I’m facing a significant issue when using better-auth in a public package that must be ESM/CJS compatible. To avoid CJS/ESM incompatibility (which causes the entire package to break at import time, even if the code path is never reached), I must use dynamic import (await import('better-auth/client')) for createAuthClient. See related issue 3613 comment.

Minimal Example:

// (Context: I cannot use static import for createAuthClient, because it breaks CJS consumers.)

let authClientInstance = null;

const getAuthClient = async () => {
  if (authClientInstance) return authClientInstance;
  const { createAuthClient } = await import("better-auth/client");
  authClientInstance = createAuthClient({ baseURL: "http://localhost:3100" });
  return authClientInstance;
};

const signInEmail = async (...args) => {
  const client = await getAuthClient();
  return client.signIn.email(...args);
};

Issue:
When I call one of this function, I get the following network error in the browser:

GET http://localhost:3100/api/auth/then 404 (Not Found)

After investigating, it appears that the proxy returned by createAuthClient is being treated as a Promise by the JavaScript runtime (or some internal code), and thus tries to access the .then property (Promise-like). Because the proxy does not implement a then method, this causes the proxy to treat then as a regular API path, resulting in an HTTP request to /api/auth/then.

createDynamicPathProxy code path: packages/better-auth/src/client/vanilla.ts

Why dynamic import?
If I use a static import of better-auth/client, it breaks CJS consumers even if the code is never called due to how Node/ESM interop works. (Static import loads the ESM module at load-time.)
Using dynamic import should solve this, but due to the proxy, it breaks functionally.

Request:

  • Can you ensure the object returned by createAuthClient is not mistaken for a Promise (i.e., it should not have a then property unless it actually implements Promise behavior)?
  • Or alternatively, is there a recommended pattern for using createAuthClient only with dynamic import in public packages without breaking the proxy API?
Originally created by @aymericzip on GitHub (Jul 27, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3650 Hi, I’m facing a significant issue when using `better-auth` in a public package that must be ESM/CJS compatible. To avoid CJS/ESM incompatibility (which causes the entire package to break at import time, even if the code path is never reached), I **must** use dynamic import (`await import('better-auth/client')`) for `createAuthClient`. See related [issue 3613 comment](https://github.com/better-auth/better-auth/issues/3613#issuecomment-3123375720). **Minimal Example:** ```ts // (Context: I cannot use static import for createAuthClient, because it breaks CJS consumers.) let authClientInstance = null; const getAuthClient = async () => { if (authClientInstance) return authClientInstance; const { createAuthClient } = await import("better-auth/client"); authClientInstance = createAuthClient({ baseURL: "http://localhost:3100" }); return authClientInstance; }; const signInEmail = async (...args) => { const client = await getAuthClient(); return client.signIn.email(...args); }; ``` **Issue:** When I call one of this function, I get the following network error in the browser: ``` GET http://localhost:3100/api/auth/then 404 (Not Found) ``` After investigating, it appears that the proxy returned by `createAuthClient` is being treated as a `Promise` by the JavaScript runtime (or some internal code), and thus tries to access the `.then` property (Promise-like). Because the proxy does not implement a `then` method, this causes the proxy to treat `then` as a regular API path, resulting in an HTTP request to `/api/auth/then`. `createDynamicPathProxy` code path: `packages/better-auth/src/client/vanilla.ts` **Why dynamic import?** If I use a static import of `better-auth/client`, it breaks CJS consumers *even if the code is never called* due to how Node/ESM interop works. (Static import loads the ESM module at load-time.) Using dynamic import should solve this, but due to the proxy, it breaks functionally. **Request:** * Can you ensure the object returned by `createAuthClient` is *not* mistaken for a Promise (i.e., it should **not** have a `then` property unless it actually implements Promise behavior)? * Or alternatively, is there a recommended pattern for using `createAuthClient` *only* with dynamic import in public packages without breaking the proxy API?
GiteaMirror added the locked label 2026-04-15 16:43:52 -05:00
Author
Owner

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

This is a known pitfall with JavaScript proxies: the object returned by createAuthClient does not handle the .then property, so when something (like dynamic import or promise-aware code) checks for .then, the proxy interprets it as an API path and triggers a request to /api/auth/then (source). There’s currently no built-in workaround or recommended pattern in the docs for this scenario, and CJS compatibility is not officially supported (source).

Workaround:
You can wrap the client in another proxy that intercepts the 'then' property and returns undefined, making it non-thenable. Here’s a drop-in wrapper:

function nonThenableProxy(obj) {
  return new Proxy(obj, {
    get(target, prop, receiver) {
      if (prop === 'then') return undefined;
      return Reflect.get(target, prop, receiver);
    }
  });
}

// Usage:
const { createAuthClient } = await import("better-auth/client");
authClientInstance = nonThenableProxy(createAuthClient({ baseURL: "http://localhost:3100" }));

This prevents the proxy from being mistaken for a Promise and avoids the unwanted network call.

Long-term:
The best fix would be for better-auth to add a 'then' guard in its proxy implementation (details). Until then, the wrapper above is the safest approach.

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:3124676081 --> @dosubot[bot] commented on GitHub (Jul 27, 2025): <!-- Answer --> This is a known pitfall with JavaScript proxies: the object returned by `createAuthClient` does not handle the `.then` property, so when something (like dynamic import or promise-aware code) checks for `.then`, the proxy interprets it as an API path and triggers a request to `/api/auth/then` ([source](https://github.com/better-auth/better-auth/blob/e7af3aa80c0cce284ed7ef3b61056322e50cd964/packages/better-auth/src/client/proxy.ts)). There’s currently no built-in workaround or recommended pattern in the docs for this scenario, and CJS compatibility is not officially supported ([source](https://github.com/better-auth/better-auth/issues/3568)). **Workaround:** You can wrap the client in another proxy that intercepts the `'then'` property and returns `undefined`, making it non-thenable. Here’s a drop-in wrapper: ```js function nonThenableProxy(obj) { return new Proxy(obj, { get(target, prop, receiver) { if (prop === 'then') return undefined; return Reflect.get(target, prop, receiver); } }); } // Usage: const { createAuthClient } = await import("better-auth/client"); authClientInstance = nonThenableProxy(createAuthClient({ baseURL: "http://localhost:3100" })); ``` This prevents the proxy from being mistaken for a Promise and avoids the unwanted network call. **Long-term:** The best fix would be for better-auth to add a `'then'` guard in its proxy implementation ([details](https://github.com/better-auth/better-auth/blob/e7af3aa80c0cce284ed7ef3b61056322e50cd964/packages/better-auth/src/client/proxy.ts)). Until then, the wrapper above is the safest approach. <!-- 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/03b46297-54f6-4bb2-b32f-257adc65aa99?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/03b46297-54f6-4bb2-b32f-257adc65aa99?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/03b46297-54f6-4bb2-b32f-257adc65aa99?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/03b46297-54f6-4bb2-b32f-257adc65aa99?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/03b46297-54f6-4bb2-b32f-257adc65aa99?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/03b46297-54f6-4bb2-b32f-257adc65aa99?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/03b46297-54f6-4bb2-b32f-257adc65aa99?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/3650)
Author
Owner

@aymericzip commented on GitHub (Jul 30, 2025):

I tested similar pattern, but I will test it and drop an update

<!-- gh-comment-id:3136516271 --> @aymericzip commented on GitHub (Jul 30, 2025): I tested similar pattern, but I will test it and drop an update
Author
Owner

@dosubot[bot] commented on GitHub (Oct 29, 2025):

Hi, @aymericzip. I'm Dosu, and I'm helping the better-auth team manage their backlog and am marking this issue as stale.

Issue Summary:

  • You reported that dynamically importing createAuthClient causes the returned proxy to be mistaken for a Promise, leading to unintended .then property access and a 404 error.
  • This is a known JavaScript proxy pitfall where the proxy treats .then as an API path, causing incorrect requests.
  • A suggested workaround is to wrap the client in another proxy that returns undefined for the then property to avoid it being thenable.
  • A long-term fix would require better-auth to add a then guard in its proxy implementation.
  • You planned to test the suggested workaround and provide an update.

Next Steps:

  • Please let me know if this issue is still relevant with the latest version of better-auth by commenting here.
  • If I don’t hear back within 7 days, I will automatically close this issue.

Thanks for your understanding and contribution!

<!-- gh-comment-id:3462539400 --> @dosubot[bot] commented on GitHub (Oct 29, 2025): Hi, @aymericzip. I'm [Dosu](https://dosu.dev), and I'm helping the better-auth team manage their backlog and am marking this issue as stale. **Issue Summary:** - You reported that dynamically importing `createAuthClient` causes the returned proxy to be mistaken for a Promise, leading to unintended `.then` property access and a 404 error. - This is a known JavaScript proxy pitfall where the proxy treats `.then` as an API path, causing incorrect requests. - A suggested workaround is to wrap the client in another proxy that returns `undefined` for the `then` property to avoid it being thenable. - A long-term fix would require better-auth to add a `then` guard in its proxy implementation. - You planned to test the suggested workaround and provide an update. **Next Steps:** - Please let me know if this issue is still relevant with the latest version of better-auth by commenting here. - If I don’t hear back within 7 days, I will automatically close this issue. Thanks for your understanding and contribution!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#18304