remix-run / remix

Build Better Websites. Create modern, resilient user experiences with web fundamentals.
https://remix.run
MIT License
29.71k stars 2.5k forks source link

Server Timeout on deferred routes #7991

Closed keepyara closed 5 months ago

keepyara commented 11 months ago

What version of Remix are you using?

2.2.0

Are all your remix dependencies & dev-dependencies using the same version?

Steps to Reproduce

Follow up from this issue: https://github.com/remix-run/remix/issues/4493

  1. Create a remix project with @vercel/remix @supabase/auth-helpers-remix and tailwindcss

  2. Create a route with a loader deferring results from supabase

  3. Add cache control headers

  4. Make changes to tailwind and save

  5. Refresh the route (a couple of times to trigger the timeout)

Expected Behavior

Expected to have deferred or cached result to come through.

Actual Behavior

After refreshing the page you will experience a Server timeout error from the error boundary or Await's errorElement.

brophdawg11 commented 11 months ago

Are you able to provide a minimal reproduction? Ideally without the supabase requirement and just using promises to return deferred data. Vercel does some tricky things with their integration points so it's hard to know if this s a Remix issue or a Vercel adapter issue.

keepyara commented 11 months ago

@brophdawg11 Yes, here is a repo: https://github.com/yarapolana/reproductible-example

For full context, I think Sentry should be included as I've seen that could be related to this issue. https://github.com/getsentry/sentry-javascript/issues/7332

taochu commented 10 months ago

Yeah encountering same issue over here w prisma

chopfitzroy commented 7 months ago

Seeing this intermittently as well.

vitormarkis commented 7 months ago

same here, i was just testing out remix

i deferred a setTimeout promise and the server times out in the client

actually resolves the promise but can't swap the UI

brophdawg11 commented 7 months ago

@keepyara What am I supposed to do in that reproduction? There's a handful of different routes all using suspense - all of which are working OK for me - but the styles don't seem to be applying so I can't do the Make changes to tailwind and save step above.

Could you further simplify that by removing the turborepo stuff and just start with a bare-bones Remix app with a single route exhibiting the problem? And then provide the steps to trigger the problem?

yarapolana commented 7 months ago

@brophdawg11 Thats great that my repo works on your side, I will minimize the repo for you to retry the issue, though the instructions in the original comment are clear which fall into the "bare-bones" project that you describe.

wKovacs64 commented 7 months ago

It only happens if the deferred data takes a sufficiently long time (5 seconds, in my testing).

@brophdawg11 Here you go, bro(ph):

https://stackblitz.com/edit/remix-run-remix-ozkevh?file=app%2Froutes%2Fdefer.tsx

brophdawg11 commented 7 months ago

That was my original hunch but the reproducible example only deferred for 3 seconds so I assumed that was not the cause 🤷

If this is indeed the root cause for OP, you control the length of this timeout via the <RemixServer abortDelay> prop which defaults to 5 seconds in entry.server.tsx. If you don't have an app/entry.server.tsx fiel, you can run npx remix reveal entry.server to create it and then make the edit accordingly

yarapolana commented 7 months ago

@brophdawg11 This was my initial solution but exposing the entry.server.tsx breaks the application when using Vercel.

brophdawg11 commented 7 months ago

Have you filed an issue on the Vercel side? I'm not sure what Remix can do to fix that if Vercel doesn't let you access the entry.server in their deployment setup?

yarapolana commented 7 months ago

I ended up updating remix libs to 2.8.0 and this hasn't come up yet, no changes were made besides this. Will close this once I get to the account that opened the issue.

stellan-s commented 7 months ago

I'm getting this error as well, on 2.8.0. When I save changes to my project and the page reloads (or if I do a manual refresh), I always get the "Server timeout" error on routes that fetches from a 3rd party api. ABORT_DELAY is set to 20_000 just for testing. After navigating to another route and then back again, it starts working. Edit: both when running locally and in azure cloud.

guelosuperstart commented 7 months ago

HEllo, i have the same issue ⬆️

with :

├── @remix-run/dev@2.8.0
├── @remix-run/eslint-config@2.8.0
├── @remix-run/express@2.8.0
├── @remix-run/node@2.8.0
├── @remix-run/react@2.8.0
├── @remix-run/serve@2.8.0
├── @tailwindcss/forms@0.5.7
├── @types/luxon@3.4.2
├── @types/react-dom@18.2.20
├── @types/react@18.2.64
├── html-to-react@1.7.0
├── isbot@5.1.1
├── npm@10.5.0
├── react-dom@18.2.0
├── react@18.2.0
├── remix-toast@1.2.0
├── remix@2.8.0
├── typescript@5.4.2
ai-niket commented 6 months ago

We are facing same issue when defer fetches third party data, probably related to timeout as mentioned by some.

rnwonder commented 6 months ago

Can confirm issue still exists when you reload a page with streaming. If you navigate from a different page to this page it works well but If you refresh this page it throws time out error. Opening this page link on a different tab throws error

here is my example

export async function loader() {
  const getData = async () => {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    return {
      name: "Product name",
      image: `https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQo4sBM1qm7OEiInjW5oZVF4q67s-HxnVzRL1kEagILpQ&s`,
    };
  };

  const query = getData();

  return defer({
    query,
  });
}

export default function Test() {
  const { query } = useLoaderData<typeof loader>();
  return (
    <div>
      <h1>Test</h1>

      <Suspense fallback={<p>Hey cool streaming on remix</p>}>
        <Await resolve={query}>
          {(data) => (
            <>
              <h2>{data?.name}</h2>
              <Image src={data?.image} alt={data?.name} />
            </>
          )}
        </Await>
      </Suspense>
    </div>
  );
}
abhishek-prabhakar commented 6 months ago

I'm also experiencing the same issue with deferred data. I tried with higher ABORT_DELAY value but nothing changed.

ItamarShDev commented 5 months ago

this error is making the whole streaming feature redundant imho.

thuupx commented 5 months ago

Remix 2.6.0 still has this issue

Tjerk-Haaye-Henricus commented 5 months ago

We have the same issue are there any plans to come over it ? This issue is open since November 2023 isn't it slowly time to solve it ?

brophdawg11 commented 5 months ago

Can someone clarify what needs to be solved here?

The answer for extending the streaming timeout is to use <RemixServer abortDelay>. OP's issue was since resolved and was likely an issue with configuration/setup on Vercel or package deps mismatches. The only other reproduction doesn't produce any errors for me on a local app.

Can someone provide a working minimal reproduction and clear steps to trigger the issue (via stackblitz or a github repo)? Otherwise I will close this out in a few days since the OP issue has been resolved.

stellan-s commented 5 months ago

In my case the abortdelay was indeed set too low. The confusion was that a timeout wasn't triggered if I navigated to another route and then back again, causing me to believe that a navigation "fixed" the issue. Not very easy to explain.

What version of Remix are you using?

2.9.1

Are all your remix dependencies & dev-dependencies using the same version?

Yes

Steps to reproduce

  1. If I set the ABORT_DELAY to 5000
    
    const ABORT_DELAY = 5_000;

2. and then create two routes like this:

```typescript
import { defer } from "@remix-run/node";
import { useLoaderData, Await } from "@remix-run/react";
import { Suspense } from "react";

export async function loader() {
  const query = fetch('https://mocki.io/v1/a6b5ff21-0aa1-455a-8101-ac2a2b1cc206',
    {
      method: "GET",
      headers: {
      },
      redirect: "follow",
    },)
    .then(res => {
      return new Promise((resolve) => {
       // a delay here...
        setTimeout(() => {
          resolve(res)
        }, 12000)
      })
    })
    .then(res => res.json())
  return defer({
    query,
  });
}

export default function UnCool() {
  const { query } = useLoaderData<typeof loader>();
  return (
    <div>
      <h1>Test 123</h1>
      <Suspense fallback={<p>Loading...</p>}>
        <Await resolve={query}>
          {(data) => (
            <>
              <h2>{data?.name}</h2>
              {JSON.stringify(data)}
            </>
          )}
        </Await>
      </Suspense>
    </div>
  );
}

and this:

import { defer, json } from "@remix-run/node";
export async function loader() {
  return json({
    message: "OK",
  });
}

export default function Cool() {
  return (
    <div>
      <h1>Everything is cool here</h1>
    </div>
  );
}
  1. and then navigate to '/uncool' I will get a timeout. No surprises there.
  2. While the timeout error message is displayed, push the browsers back button (or click a link in a real world example) and then go forward again, back to the "uncool" route.

Expected behaviour

Another timeout

Actual behaviour

No timeout is triggered. The page loads until the promise resolves and the data is displayed.

Bonus

refresh the uncool route and the timeout is triggered again.

rnwonder commented 5 months ago

Can someone clarify what needs to be solved here?

The answer for extending the streaming timeout is to use <RemixServer abortDelay>. OP's issue was since resolved and was likely an issue with configuration/setup on Vercel or package deps mismatches. The only other reproduction doesn't produce any errors for me on a local app.

Can someone provide a working minimal reproduction and clear steps to trigger the issue (via stackblitz or a github repo)? Otherwise I will close this out in a few days since the OP issue has been resolved.

Hello there thanks for taking the time to look at this. If you scroll up you will see my code example of the issue.

When you add streaming to a page and navigate to the page say via a Link component it streams perfectly. But when you try refreshing the page you get a timeout error. If you copy the page url and open in a different tab you get timeout error as well.

Currently the streaming as implemented in the code snippet I posted above only works when you navigate to the page from another page.

brophdawg11 commented 5 months ago

ok - this looks like it's just a bit of confusion around what abortDelay does. abortDelay is strictly about the initial server-side render of an HTML document. It's used for 2 things:

  1. Aborting the renderToPipeableStream call
  2. Setting a timeout to automatically reject pending streaming promises in the initial document render via <RemixServer>

renderToPipeableStream and <RemixServer> are out of the picture after the initial document render, so that abortDelay value has no impact on subsequent client side navigations that load deferred data through ?_data requests. Those can take as long as they want (until your server/browser cancels any open connection) and still resolve.

So the answer is to set your abortDelay value accordingly for HTML document renders. If you are getting timeouts, increase the value.

The upcoming (currently unstable) Single Fetch feature replaces deprecates defer with a more powerful streaming format which gives you a new timeout mechanism that works the same on both document and data requests, so I would recommend updating to that when it lands as stable (or before if you want!) to normalize these behaviors.

rnwonder commented 5 months ago

ok - this looks like it's just a bit of confusion around what abortDelay does. abortDelay is strictly about the initial server-side render of an HTML document. It's used for 2 things:

  1. Aborting the renderToPipeableStream call
  2. Setting a timeout to automatically reject pending streaming promises in the initial document render via <RemixServer>

renderToPipeableStream and <RemixServer> are out of the picture after the initial document render, so that abortDelay value has no impact on subsequent client side navigations that load deferred data through ?_data requests. Those can take as long as they want (until your server/browser cancels any open connection) and still resolve.

So the answer is to set your abortDelay value accordingly for HTML document renders. If you are getting timeouts, increase the value.

The upcoming (currently unstable) Single Fetch feature replaces deprecates defer with a more powerful streaming format which gives you a new timeout mechanism that works the same on both document and data requests, so I would recommend updating to that when it lands as stable (or before if you want!) to normalize these behaviors.

This definitely put things in perspective thank you!

However, I believe the default timeout duration is 5s right? Last time I tested I believe I also increased the timeout to 10s after finding this pr but still got a timeout error.

If you would refer to the code snippet I used above the promise call being deferred was delayed by only 1s which should be enough time if I am not mistaken for it to be resolved before the timeout duration of 5s.

That's where the issue was for me at least.

rnwonder commented 5 months ago

@brophdawg11

Finally had the time to test this out again. I upgraded to the latest version(2.9.1) and tested again. It works flawlessly now!

guelosuperstart commented 5 months ago

HEllo @rnwonder,

How did you get it to work? I still have the error on my side in my dev environment (local)

All my components are up to date :

├── @remix-run/dev@2.9.2
├── @remix-run/eslint-config@2.9.2
├── @remix-run/express@2.9.2
├── @remix-run/node@2.9.2
├── @remix-run/react@2.9.2
├── @remix-run/serve@2.9.2
├── react-dom@18.3.1
├── react@18.3.1
├── remix@2.9.2
├── vite-tsconfig-paths@4.3.2
├── vite@5.2.11

example :


export async function api1(period?: string): Promise<any> {
    await new Promise(resolve => setTimeout(resolve, 1000));
    return {success : true};
}

export async function loader({ request }: LoaderFunctionArgs) {
  const api1 =  api1(); -> call my external backend with a delay of 1 seconde
  const api2 =  await api2(); call my external backend
  const api3 =  await api3(); call my external backend

  return defer({ api1, api2, api3 });
}

export default function Component() {
  const { user }: { user: UserSession } = useAppLoaderData();
  const { api1, api2, api3 } = useLoaderData<typeof loader>();
    return (
    <>
            <Suspense fallback={'Loading...'}>
            <Await resolve={api1}>
              {(data) =>
                <Component2 data={data} />}
            </Await>
          </Suspense>
    </>
  );
}

if i navigate on my site everything is ok, but if i reload the page i got Server timeout.

In my entry.server : const ABORT_DELAY = 30_000;

i migrated my application to vite.

Thank you for your advice.

stellan-s commented 5 months ago

Yes, the reload (and reload on save) does indeed cause an immediate timeout. Same here.

rnwonder commented 5 months ago

@guelosuperstart

Hi there so for me I am not really sure what fixed it because I could have sworn when I increase mine to 10_000 when I tested few weeks back I still got the same timeout error on reload. But upgrading just fixed it.

I just created a new remix project now, added a test route with my example above got the timeout error for the default 5_000 abort delay.

when I increased this to any number above this it works. Maybe try it out on your end. These are the dependencies for the new remix app

  "dependencies": {
    "@remix-run/node": "^2.9.2",
    "@remix-run/react": "^2.9.2",
    "@remix-run/serve": "^2.9.2",
    "isbot": "^4.1.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@remix-run/dev": "^2.9.2",
    "@types/react": "^18.2.20",
    "@types/react-dom": "^18.2.7",
    "@typescript-eslint/eslint-plugin": "^6.7.4",
    "@typescript-eslint/parser": "^6.7.4",
    "eslint": "^8.38.0",
    "eslint-import-resolver-typescript": "^3.6.1",
    "eslint-plugin-import": "^2.28.1",
    "eslint-plugin-jsx-a11y": "^6.7.1",
    "eslint-plugin-react": "^7.33.2",
    "eslint-plugin-react-hooks": "^4.6.0",
    "typescript": "^5.1.6",
    "vite": "^5.1.0",
    "vite-tsconfig-paths": "^4.2.1"
  },
guelosuperstart commented 4 months ago

Hello @rnwonder

I did some tests and even with this simple code, i have a time Server timeout :/ :

import { defer } from '@remix-run/node';
import { Suspense } from 'react';
import { Await, useLoaderData } from '@remix-run/react';

export const loader = async () => {
  const promiseOne = new Promise((resolve) => {
    setTimeout(() => {
      const numDocs = Math.floor(Math.random() * 10);
      resolve({ num: numDocs });
    }, 1000);
  }) as Promise<{ num: number }>;

  return defer({ promiseOne });
};

const TestPage = () => {
  const data = useLoaderData<typeof loader>();

  return (
    <Suspense fallback={<span> {`Loading... `} </span>}>
      <Await resolve={data.promiseOne} errorElement={<p>Error executing job!</p>}>
        {(val) => (<div>{val.num}</div>)}
      </Await>
    </Suspense>
  );
};

export default TestPage;
{
    "@remix-run/express": "^2.9.2",
    "@remix-run/node": "^2.9.2",
    "@remix-run/react": "^2.9.2",
    "@remix-run/serve": "^2.9.2",
    "react-dom": "^18.3.1",
    "remix": "^2.9.2",
},
{
    "@remix-run/dev": "^2.9.2",
    "@types/react": "^18.3.3",
    "@types/react-dom": "^18.3.0",
    "vite": "^5.2.12",
    "vite-tsconfig-paths": "^4.3.2"
}
entry.server.ts ```ts import { PassThrough } from 'node:stream'; import type { AppLoadContext, EntryContext } from '@remix-run/node'; import { createReadableStreamFromReadable } from '@remix-run/node'; import { RemixServer } from '@remix-run/react'; import { isbot } from 'isbot'; import { renderToPipeableStream } from 'react-dom/server'; import { createInstance } from 'i18next'; import { I18nextProvider, initReactI18next } from 'react-i18next'; import i18next from '~/localization/i18n.server'; import i18n from '~/localization/i18n'; import { returnLanguageIfSupported } from '~/localization/resource'; const ABORT_DELAY = 30_000; export default function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext, // This is ignored so we can keep it in the template for visibility. Feel // free to delete this parameter in your app if you're not using it! // eslint-disable-next-line @typescript-eslint/no-unused-vars loadContext: AppLoadContext, ) { return isbot(request.headers.get('user-agent') || '') ? handleBotRequest( request, responseStatusCode, responseHeaders, remixContext, ) : handleBrowserRequest( request, responseStatusCode, responseHeaders, remixContext, ); } async function handleBotRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext, ) { const url = new URL(request.url); const { pathname } = url; const lang = pathname.split('/')[1]; const instance = createInstance(); let lng = returnLanguageIfSupported(lang) ?? (await i18next.getLocale(request)); const ns = i18next.getRouteNamespaces(remixContext); await instance.use(initReactI18next).init({ ...i18n, lng, ns, }); return new Promise((resolve, reject) => { let shellRendered = false; const { pipe, abort } = renderToPipeableStream( , { onAllReady() { shellRendered = true; const body = new PassThrough(); const stream = createReadableStreamFromReadable(body); responseHeaders.set('Content-Type', 'text/html'); resolve( new Response(stream, { headers: responseHeaders, status: responseStatusCode, }), ); pipe(body); }, onShellError(error: unknown) { reject(error); }, onError(error: unknown) { responseStatusCode = 500; // Log streaming rendering errors from inside the shell. Don't log // errors encountered during initial shell rendering since they'll // reject and get logged in handleDocumentRequest. if (shellRendered) { console.error(error); } }, }, ); setTimeout(abort, ABORT_DELAY); }); } async function handleBrowserRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext, ) { const url = new URL(request.url); const { pathname } = url; const lang = pathname.split('/')[1]; const instance = createInstance(); let lng = returnLanguageIfSupported(lang) ?? (await i18next.getLocale(request)); const ns = i18next.getRouteNamespaces(remixContext); await instance.use(initReactI18next).init({ ...i18n, lng, ns, }); return new Promise((resolve, reject) => { let shellRendered = false; const { pipe, abort } = renderToPipeableStream( , { onShellReady() { shellRendered = true; const body = new PassThrough(); const stream = createReadableStreamFromReadable(body); responseHeaders.set('Content-Type', 'text/html'); resolve( new Response(stream, { headers: responseHeaders, status: responseStatusCode, }), ); pipe(body); }, onShellError(error: unknown) { reject(error); }, onError(error: unknown) { responseStatusCode = 500; // Log streaming rendering errors from inside the shell. Don't log // errors encountered during initial shell rendering since they'll // reject and get logged in handleDocumentRequest. if (shellRendered) { console.error(error); } }, }, ); setTimeout(abort, ABORT_DELAY); }); } ```
cuihaoweb commented 1 month ago

@rnwonder @keepyara I have found the cause, you need to close your browser plugin or browse through it in trackless mode. I guess, some plugin changed the content of the page and it made hydrate an exception. When I used trackless browsing, everything is fine. It will change the content of my page, cause hydrate to be abnormal, and then defer will timeout, I'm not sure how exactly,

This is what happens when I open the dark reader chrome plugin

image

When I closed everything was fine:

image

But I think this is not a good solution, because I can not ensure whether other users use some plug-ins, I hope the official to give a reasonable solution

rnwonder commented 1 month ago

cc @brophdawg11

arjunyel commented 4 weeks ago

@rnwonder @keepyara I have found the cause, you need to close your browser plugin or browse through it in trackless mode. I guess, some plugin changed the content of the page and it made hydrate an exception. When I used trackless browsing, everything is fine. It will change the content of my page, cause hydrate to be abnormal, and then defer will timeout, I'm not sure how exactly,

This is what happens when I open the dark reader chrome plugin image

When I closed everything was fine: image

But I think this is not a good solution, because I can not ensure whether other users use some plug-ins, I hope the official to give a reasonable solution

@cuihaoweb Updating to React 19 should fix this

vpaivag commented 3 weeks ago

Still no solution? I have the same issue, using a private tab solves the issue, but cannot assure that my users will use a private tab

davidesigner commented 3 weeks ago

Same here! With React Canary.

brophdawg11 commented 2 weeks ago

The OP's issue is fixed which is why this issue is closed.

It sounds like the rest of the issues here are different issues and being caused by certain browser extensions. If these are minimally reproducible, please open a new issue with a working stackblitz that exhibits the issue and (if necessary) indicate which browser extension causes the issue (freely available and not requiring a login).

I will say though that if this is caused by a browser extension behavior, there's no promise Remix can do anything about it and I would also suggest investigating this issue with the bowser extension author.

lumiuko commented 2 weeks ago

npm i react@rc react-dom@rc This solved the issue for me