[Feature] - Add customFetcher option for the client-side #371

Closed
opened 2026-03-13 07:43:39 -05:00 by GiteaMirror · 10 comments
Owner

Originally created by @serban-mihai on GitHub (Dec 8, 2024).

This is not a priority, but it would be nice to be possible to use custom fetch functions instead of better-fetch.
I'm not sure if this is possible on the structural level but I find myself in a situation where I need the client-side library to talk to the server-side through a different fetcher because the backend where better-auth lives is not accessible on the public internet.

I have a Cloudflare Worker with better-auth that is only accessible through Service Bindings from other Workers (or Pages Functions) for security reasons.
It would be nice if instead of providing the client side a baseUrl I could provide a customFetcher instead, since I could use the env.MY_SERVICE.fetch() handler and the routing inside the backend will just map to all the API nicely, allowing me to use the client-side handler without any hustle.

Originally created by @serban-mihai on GitHub (Dec 8, 2024). This is not a priority, but it would be nice to be possible to use custom fetch functions instead of `better-fetch`. I'm not sure if this is possible on the structural level but I find myself in a situation where I need the client-side library to talk to the server-side through a different fetcher because the backend where `better-auth` lives is not accessible on the public internet. I have a Cloudflare Worker with `better-auth` that is only accessible through [Service Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) from other Workers (or Pages Functions) for security reasons. It would be nice if instead of providing the client side a `baseUrl` I could provide a `customFetcher` instead, since I could use the `env.MY_SERVICE.fetch()` handler and the routing inside the backend will just map to all the API nicely, allowing me to use the client-side handler without any hustle.
Author
Owner

@Bekacru commented on GitHub (Dec 8, 2024):

you can pass custom fetcher as

const authClient = createAuthClient({
    fetchOptions: {
        customFetchImpl: //pass custom fetch implementation 
   }
})
@Bekacru commented on GitHub (Dec 8, 2024): you can pass custom fetcher as ```ts const authClient = createAuthClient({ fetchOptions: { customFetchImpl: //pass custom fetch implementation } }) ```
Author
Owner

@serban-mihai commented on GitHub (Dec 8, 2024):

Thanks for the quick answer @Bekacru!

I've seen that key before while looking at the docs for both better-auth and better-fetch but it seems typescript doesn't like it or I'm the one not implementing it right:

import { createAuthClient } from "better-auth/client";

type AuthService = import("@cloudflare/workers-types").Service;
type Env = {
  AUTH_SERVICE: AuthService;
};

const { fetch } = env.AUTH_SERVICE as AuthService;
const authClient = createAuthClient({
  fetchOptions: {
    customFetchImpl: fetch, // Type Error
  },
});

The Type error looks like:

Type '(input: URL | RequestInfo<unknown, CfProperties<unknown>>, init?: RequestInit<CfProperties<unknown>> | undefined) => Promise<Response>' is not assignable to type 'FetchEsque'.
  Types of parameters 'input' and 'input' are incompatible.
    Type 'string | URL | Request' is not assignable to type 'URL | RequestInfo<unknown, CfProperties<unknown>>'.
      Type 'Request' is not assignable to type 'URL | RequestInfo<unknown, CfProperties<unknown>>'.
        Type 'Request' is missing the following properties from type 'Request<unknown, CfProperties<unknown>>': fetcher, bytes

There is a signature mismatch between better-auth and cloudflare types

type FetchEsque = (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response>; // better-auth
type fetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>; // cloudflare-workers-types

I haven't tried ignoring typescript yet, but I want to be sure I'm not making any mistake in using the customFetchImpl before.

Is it the right way to assign the custom fetcher or am I doing it wrong?

@serban-mihai commented on GitHub (Dec 8, 2024): Thanks for the quick answer @Bekacru! I've seen that key before while looking at the docs for both `better-auth` and `better-fetch` but it seems typescript doesn't like it or I'm the one not implementing it right: ```typescript import { createAuthClient } from "better-auth/client"; type AuthService = import("@cloudflare/workers-types").Service; type Env = { AUTH_SERVICE: AuthService; }; const { fetch } = env.AUTH_SERVICE as AuthService; const authClient = createAuthClient({ fetchOptions: { customFetchImpl: fetch, // Type Error }, }); ``` The Type error looks like: ``` Type '(input: URL | RequestInfo<unknown, CfProperties<unknown>>, init?: RequestInit<CfProperties<unknown>> | undefined) => Promise<Response>' is not assignable to type 'FetchEsque'. Types of parameters 'input' and 'input' are incompatible. Type 'string | URL | Request' is not assignable to type 'URL | RequestInfo<unknown, CfProperties<unknown>>'. Type 'Request' is not assignable to type 'URL | RequestInfo<unknown, CfProperties<unknown>>'. Type 'Request' is missing the following properties from type 'Request<unknown, CfProperties<unknown>>': fetcher, bytes ``` There is a signature mismatch between `better-auth` and `cloudflare` types ```typescript type FetchEsque = (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response>; // better-auth type fetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>; // cloudflare-workers-types ``` I haven't tried ignoring typescript yet, but I want to be sure I'm not making any mistake in using the `customFetchImpl` before. Is it the right way to assign the custom fetcher or am I doing it wrong?
Author
Owner

@Bekacru commented on GitHub (Dec 8, 2024):

yeah I don't think cf fetch matches one to one with the default fetch interface. try to ignore it and see if it works if not try to pass the arguments explicitly.

@Bekacru commented on GitHub (Dec 8, 2024): yeah I don't think cf fetch matches one to one with the default fetch interface. try to ignore it and see if it works if not try to pass the arguments explicitly.
Author
Owner

@serban-mihai commented on GitHub (Dec 9, 2024):

I tried ignoring typescript errors but nothing unfortunately.
Some more details on how to usecustomFetchImpl would be much appreciated.

@serban-mihai commented on GitHub (Dec 9, 2024): I tried ignoring typescript errors but nothing unfortunately. Some more details on how to use`customFetchImpl` would be much appreciated.
Author
Owner

@Bekacru commented on GitHub (Dec 9, 2024):

customFetchImpl accepts standard web fetch implementation. Meaning it passes two arguments to the implementation url and requestInit which contains the body, headers....so basically you can explicitly call the cf fetch impl by passing the url and the request init object as cf fetch expects it.

@Bekacru commented on GitHub (Dec 9, 2024): customFetchImpl accepts standard web fetch implementation. Meaning it passes two arguments to the implementation `url` and `requestInit` which contains the body, headers....so basically you can explicitly call the cf fetch impl by passing the url and the request init object as cf fetch expects it.
Author
Owner

@serban-mihai commented on GitHub (Dec 9, 2024):

Update:
I did make it work eventually but the type errors are still there. this is the current setup:

const authClient = createAuthClient({
  fetchOptions: {
    baseURL: "<your-base-url-with-path-if-any>", // Although you're not gonna call it directly, you still need it for routing purposes inside the Service Binding Worker
    customFetchImpl: (input: URL, options: RequestInit) =>
      AUTH_SERVICE.fetch(new URL(input), options), // Isolated type error to options only
  },
});

The type error now only references the body mismatch between better-fetch and cloudflare workers fetch

Argument of type 'RequestInit' is not assignable to parameter of type 'RequestInit<CfProperties<unknown>>'.
  Types of property 'body' are incompatible.

I noticed that the authClient doesn't have an explicit typescript type to assign, or I don't know what that should be.

@serban-mihai commented on GitHub (Dec 9, 2024): **Update:** I did make it work eventually but the type errors are still there. this is the current setup: ```typescript const authClient = createAuthClient({ fetchOptions: { baseURL: "<your-base-url-with-path-if-any>", // Although you're not gonna call it directly, you still need it for routing purposes inside the Service Binding Worker customFetchImpl: (input: URL, options: RequestInit) => AUTH_SERVICE.fetch(new URL(input), options), // Isolated type error to options only }, }); ``` The type error now only references the `body` mismatch between `better-fetch` and `cloudflare workers fetch` ``` Argument of type 'RequestInit' is not assignable to parameter of type 'RequestInit<CfProperties<unknown>>'. Types of property 'body' are incompatible. ``` I noticed that the `authClient` doesn't have an explicit typescript type to assign, or I don't know what that should be.
Author
Owner

@daveycodez commented on GitHub (Dec 22, 2024):

@serban-mihai

try this?

    fetchOptions: {
        customFetchImpl: (input: string | URL | globalThis.Request, init?: RequestInit) => {
            AUTH_SERVICE.fetch(new URL(input), {...options}),
        }
    },

although it looks like "body" isn't in the right format from your error. Try the method above and see if that body error is still there, then probably need to typecast or parse the body to the correct format if it is.

It looks like the library must have been updated because FetchEsque is like this now

type FetchEsque = (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response>;

You might need to check the typeof input and only parse it to URL if it's a string otherwise pass it directly.

for body fix...

{...options, body: (options.body as ???)}
@daveycodez commented on GitHub (Dec 22, 2024): @serban-mihai try this? ```ts fetchOptions: { customFetchImpl: (input: string | URL | globalThis.Request, init?: RequestInit) => { AUTH_SERVICE.fetch(new URL(input), {...options}), } }, ``` although it looks like "body" isn't in the right format from your error. Try the method above and see if that body error is still there, then probably need to typecast or parse the body to the correct format if it is. It looks like the library must have been updated because FetchEsque is like this now ```ts type FetchEsque = (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response>; ``` You might need to check the typeof input and only parse it to URL if it's a string otherwise pass it directly. for body fix... ```ts {...options, body: (options.body as ???)} ```
Author
Owner

@serban-mihai commented on GitHub (Dec 22, 2024):

Hey, thanks for the tip @daveycodez!
Unfortunatelly while testing I've found another potential blocker for such an approach:
It looks like the browser is expecting an HTTP request in order to correctly set auth cookies, if we call the auth service from bindings then we get back the session and user values but the browser isn't setting any cookie, aka no further actions against the API from the client-side are allowed.

I didn't have time to debut this more in-depth but I'm wondering if there's a way to set the cookie once retrieved through Service Bindings without browsers complaining, the nightmare part was to deal with CORS, this might require some heavy testing before assuring it can work, but if it can, that would be a killer for performance/costs over Cloudflare deployments.

Hope to get to it in the future, for now, I've gone back to the standard requests over HTTP.

@serban-mihai commented on GitHub (Dec 22, 2024): Hey, thanks for the tip @daveycodez! Unfortunatelly while testing I've found another potential blocker for such an approach: It looks like the browser is expecting an HTTP request in order to correctly set auth cookies, if we call the auth service from bindings then we get back the `session` and `user` values but the browser isn't setting any cookie, aka no further actions against the API from the client-side are allowed. I didn't have time to debut this more in-depth but I'm wondering if there's a way to set the cookie once retrieved through Service Bindings without browsers complaining, the nightmare part was to deal with CORS, this might require some heavy testing before assuring it can work, but if it can, that would be a killer for performance/costs over Cloudflare deployments. Hope to get to it in the future, for now, I've gone back to the standard requests over HTTP.
Author
Owner

@daveycodez commented on GitHub (Dec 22, 2024):

Are you receiving the set-cookie headers from that response? I haven't used service bindings but this might help. This is how I call my public API from Capacitor running locally and get the cookies to set correctly

auth.ts

    trustedOrigins: ["http://localhost:3000"],
    advanced: {
        defaultCookieAttributes: {
            sameSite: "none",
            secure: true
        }
    },

For me I handle CORS in Next.js middleware so that part was simple enough, IDK what that entails in regards to Cloudflare.

@daveycodez commented on GitHub (Dec 22, 2024): Are you receiving the set-cookie headers from that response? I haven't used service bindings but this might help. This is how I call my public API from Capacitor running locally and get the cookies to set correctly auth.ts ```ts trustedOrigins: ["http://localhost:3000"], advanced: { defaultCookieAttributes: { sameSite: "none", secure: true } }, ``` For me I handle CORS in Next.js middleware so that part was simple enough, IDK what that entails in regards to Cloudflare.
Author
Owner

@serban-mihai commented on GitHub (Dec 22, 2024):

While testing this and when I opened the issue I was using Astro with an Hono backend where the better-auth server was deployed, and I had already the defaultCookieAttributes set, but on this level, many things get to influence the behavior of the app, browsers, CORS policy, domains/subdomain where the server and the client run and so on...

I'm now trying with a SolidStart-only solution, if you use Next.js I assume you might be deploying over Vercel, that would be another overall environment, Cloudflare has some more caveats to run around.

Thanks for reaching up though, I appreciate it!
I'll keep this updated if I find a viable solution.

@serban-mihai commented on GitHub (Dec 22, 2024): While testing this and when I opened the issue I was using Astro with an Hono backend where the `better-auth` server was deployed, and I had already the `defaultCookieAttributes` set, but on this level, many things get to influence the behavior of the app, browsers, CORS policy, domains/subdomain where the server and the client run and so on... I'm now trying with a SolidStart-only solution, if you use Next.js I assume you might be deploying over Vercel, that would be another overall environment, Cloudflare has some more caveats to run around. Thanks for reaching up though, I appreciate it! I'll keep this updated if I find a viable solution.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#371