router icon indicating copy to clipboard operation
router copied to clipboard

Tanstack-start on Cloudflare: ServerFn not working with API routes

Open cherishh opened this issue 6 months ago β€’ 22 comments

Which project does this relate to?

Start

Describe the bug

This probably is an issue only related to Cloudflare Workers

  1. Create an API route /api/test;
  2. In page /count, do the following:
  • Create a route loader. In loader, call serverFn const count = await getCountServerFn().
  • In getCountServerFn, simply fetch(${BASE_URL}/api/test)
  1. In local development, everything is ok. Soft navigation to /count, or do a hard refresh on /count, I can see the count number being rendered.
  2. Build preview, everything is still ok.
  3. Deploy to Cloudflare Works as is.
  4. On production, no matter do a soft navigation to /count or do a hard refresh, it will resulting an error.

Additional information(all in production): For simplicity I used page /count for illustration. Below you can see the actual page is /user/deprecated

  • Soft navigate to page:
Image Image
  • Hard refresh:
Image Image
  • API route itself seems fine. I can fetch this API point from frontend with no problem(fetch in useEffect).

Your Example Website or App

https://tanstack-start-on-workers-v0.tuxi.workers.dev/user/deprecated

Steps to Reproduce the Bug or Issue

As mentioned above

Expected behavior

I can call API routes just fine in serverFn on Cloudflare

Screenshots or Videos

As mentioned above

Platform

  • OS: Mac OS Sequoia 15.4.1
  • Browser: Chrome 137.0.7151.41
  • Version: 1.116.1

Additional context

/routes/user/deprecated.tsx

import { createFileRoute } from '@tanstack/react-router';
import { createServerFn } from '@tanstack/react-start';
import { BASE_URL } from '@/utils/base-url';

const getCountServerFn = createServerFn({ method: 'GET' }).handler(async () => {
  const res = await fetch(`${BASE_URL}/api/test`);
  if (!res.ok) {
    throw new Error(`api/test failed: ${res.status} ${res.statusText}`);
  }
  return res.json();
});

export const Route = createFileRoute('/user/deprecated')({
  loader: async ({ context }) => {
    const count = await getCountServerFn();

    return { count };
  },
  component: RouteComponent,
  errorComponent: () => <div>error</div>,
});

function RouteComponent() {
  const { count } = Route.useLoaderData();

  return (
    <div>
      <h2>
        <pre>{JSON.stringify(count, null, 2)}</pre>
      </h2>
    </div>
  );
}

/routes/api/test.ts

import { createAPIFileRoute } from '@tanstack/react-start/api';
import { Redis } from '@upstash/redis/cloudflare';

export const APIRoute = createAPIFileRoute('/api/test')({
  GET: async ({ request, params, env }) => {
    console.log('get test api', JSON.stringify(request));
    const redis = Redis.fromEnv(
      env ?? {
        UPSTASH_REDIS_REST_URL: 'xxx',
        UPSTASH_REDIS_REST_TOKEN: 'xxx',
      }
    );
    const count = await redis.incr('counter');
    return new Response(JSON.stringify({ count }));
  },
});

cherishh avatar May 25 '25 11:05 cherishh

This error on Cloudflare not for Start

Document: https://developers.cloudflare.com/workers/runtime-apis/fetch/

Error: Worker to Worker

akawahuynh avatar May 25 '25 18:05 akawahuynh

This error on Cloudflare not for Start

Document: https://developers.cloudflare.com/workers/runtime-apis/fetch/

Error: Worker to Worker

Thanks for the quick response! But I'm not following, why is it a Worker to Worker issue? Start As a full stack framework, I thought I just have one Worker, which my Start project runs on. If it's a Worker to Worker issue, what is Worker A and what is Worker B?

cherishh avatar May 26 '25 03:05 cherishh

cloudflare worker is implemented like you run javascript script once per request, they intercept all requests as callback to itself to avoid infinite loop, you can only do it when it is bound to a new worker (as per their docs that I can understand)

Worker A is your application (you can not call from A to A) Worker B is a new copy of A, if you want to make an end point call to /api/test

For example:

function mainA() {
    console.log(" this is worker A")
}


function mainB() {
    console.log(" this is worker B")
}
//Error will occur
//if:

function mainA() {
    console.log(" this is worker A")
    mainA()
}  //Infinite loop

//you can only do it by:
function mainB() {
    console.log(" this is worker B")
    mainA() //can only call A here
}

akawahuynh avatar May 26 '25 15:05 akawahuynh

I'm running into this also now, how would one go about circumventing this issue @akawahuynh without deploying the same code again as that seems very counter intuitive?

karthikjn01 avatar May 29 '25 10:05 karthikjn01

I'm running into this also now, how would one go about circumventing this issue @akawahuynh without deploying the same code again as that seems very counter intuitive?

you can only call api on client, but to read data from database you can call right at createServerFn()

const getCountServerFn = createServerFn({ method: 'GET' }).handler(async () => { 
const res = await db.query.... 
return res; }); 

What problem are you having, can you share it here?

akawahuynh avatar May 29 '25 16:05 akawahuynh

I'm running into this also now, how would one go about circumventing this issue @akawahuynh without deploying the same code again as that seems very counter intuitive?

you can only call api on client, but to read data from database you can call right at createServerFn()

const getCountServerFn = createServerFn({ method: 'GET' }).handler(async () => { 
const res = await db.query.... 
return res; }); 

What problem are you having, can you share it here?

Just a little heads up, one should not do mutation(post api, db update etc.) request in createServerFn. Cus loader may run multiple times.

cherishh avatar May 29 '25 17:05 cherishh

@cherishh no. you can execute a server function in any place. does not have to be a loader. can be an onClick handler if you want

schiller-manuel avatar May 29 '25 17:05 schiller-manuel

like @schiller-manuel said before, createServerFn() is like a built-in trpc in tanstack start so you can call it anywhere except API route and vice versa

akawahuynh avatar May 29 '25 20:05 akawahuynh

@akawahuynh on alpha branch you can indeed call a server function from an API route

schiller-manuel avatar May 29 '25 20:05 schiller-manuel

The issue I'm having is when I deploy to CF the api can't be called. It's a standard createserverfn not inside an api route. I get a "not found" when I try hitting the endpoint directly. However it does work in dev locally.

karthikjn01 avatar May 29 '25 20:05 karthikjn01

@karthikjn01 It's not a bug, it's a Cloudflare featureπŸ˜‚. If you run in a local environment, you're running in a node environment not CF. Have you tried building and running dev with wrangler dev? Most of the CF bug fixes are here. If there's an error, CF will definitely have the same error.

akawahuynh avatar May 29 '25 21:05 akawahuynh

I've not yet had the chance no - having said that though - if that's the case, how is tanstack start marketed as being able to deploy on CF? I'll give it a go now, but I'm pretty set on deploying w Cloudflare, so it might just be a matter of using a different framework!

karthikjn01 avatar May 29 '25 21:05 karthikjn01

@schiller-manuel Thanks. I haven't tested with alpha yet. I'm waiting for it to be stable, and have documentation to read, it's too hard to fix bugs without understanding exactly what they are 😁

akawahuynh avatar May 29 '25 21:05 akawahuynh

I've not yet had the chance no - having said that though - if that's the case, how is tanstack start marketed as being able to deploy on CF? I'll give it a go now, but I'm pretty set on deploying w Cloudflare, so it might just be a matter of using a different framework!

Yep, I still have some production apps using Start deploy to cloudflare, it's pretty stable so far, there are some bugs with the damn limit of cloudflare, if you need to render something for too long it will crash, it takes all day just to find out where the error is

For example: using better-auth, almost 503 continuously, if you want to release production saas app, you have to upgrade to paid πŸ€¦β€β™‚οΈ

akawahuynh avatar May 29 '25 21:05 akawahuynh

@cherishh no. you can execute a server function in any place. does not have to be a loader. can be an onClick handler if you want

Thanks for pointing that out! And yes, just as you said, I am indeed calling createServerFn inside the event handler now. Still, I kinda prefer calling a createServerFn inside a loader. I want the data to be ready, instead of showing a short loading state to users. Btw is it NOT recommended to call an api route inside a createServerFn?

cherishh avatar May 30 '25 01:05 cherishh

I've not yet had the chance no - having said that though - if that's the case, how is tanstack start marketed as being able to deploy on CF? I'll give it a go now, but I'm pretty set on deploying w Cloudflare, so it might just be a matter of using a different framework!

Yep, I still have some production apps using Start deploy to cloudflare, it's pretty stable so far, there are some bugs with the damn limit of cloudflare, if you need to render something for too long it will crash, it takes all day just to find out where the error is

For example: using better-auth, almost 503 continuously, if you want to release production saas app, you have to upgrade to paid πŸ€¦β€β™‚οΈ

Could you elaborate a bit more on this issue? I'm also using Better Auth (free tier) and haven't encountered any problems so far. This 503 issue is due to CF overload, or better-auth? p.s. I'm really just testing to get everything running smoothly on CF at this stage. I don't have many users.

cherishh avatar May 30 '25 01:05 cherishh

Could you elaborate a bit more on this issue? I'm also using Better Auth (free tier) and haven't encountered any problems so far. This 503 issue is due to CF overload, or better-auth?

CF workers have a limit of 10ms for free plans. For paid, it is 30s (+ up to 15min). Better Auth sometimes take compute ms of 2000ms and this is way over the 10ms limit forcing the request to fail returning 503 or the 1102 CF error.

ajxbit avatar May 30 '25 02:05 ajxbit

Could you elaborate a bit more on this issue? I'm also using Better Auth (free tier) and haven't encountered any problems so far. This 503 issue is due to CF overload, or better-auth?

CF workers have a limit of 10ms for free plans. For paid, it is 30s (+ up to 15min). Better Auth sometimes take compute ms of 2000ms and this is way over the 10ms limit forcing the request to fail returning 503 or the 1102 CF error.

Thanks for the explanation! 10ms that's some real limitationπŸ˜‚ Guess I'll upgrade to paid too.

cherishh avatar May 30 '25 04:05 cherishh

I encountered a similar issue and it took a bit of time to track down, but the fix turned out to be straightforward.

Any imports from @tanstack/react-start/server or @tanstack/solid-start/server must be used within a createServerFn context. In my case, I had separated some of my data-fetching logic into standalone fetcher functions, which included calls to getCookie from the server package. These fetcher functions were then called from within createServerFn handlers defined elsewhere.

The problem was that getCookie (and similar server-bound utilities) cannot be used outside of the createServerFn scope. To resolve the issue, I simply moved the logic that depended on getCookie directly into the createServerFn itself, rather than calling it from a helper outside its scope.

Hope this helps anyone running into the same confusion!

joeygrable94 avatar Jun 02 '25 19:06 joeygrable94

@joeygrable94 that sounds a bit strange to me, and also maybe a separate issue. can you share a complete reproducer for this?

schiller-manuel avatar Jun 02 '25 21:06 schiller-manuel

I was pretty clear in my response. You must us the server utilities inside of a serverFn. For example, CloudFlare bundler errors if I call the getCoookie method inside of an anonymous function in a component. You must place that getCookie method within a serverFn or the bundler gets mad. Unless, i'm missing something 🀷

joeygrable94 avatar Jun 04 '25 19:06 joeygrable94

yes sure you must ultimately invoke the server utilities from within the server function. however it can be done in a helper method, if that helper is invoked by the server function. calling that helper from a component will end up this helper being called on the client.

schiller-manuel avatar Jun 04 '25 19:06 schiller-manuel

@cherishh is there anything left to do here?

schiller-manuel avatar Jul 26 '25 21:07 schiller-manuel