remix
remix copied to clipboard
Server Timeout on deferred routes
What version of Remix are you using?
2.2.0
Are all your remix dependencies & dev-dependencies using the same version?
- [X] Yes
Steps to Reproduce
Follow up from this issue: https://github.com/remix-run/remix/issues/4493
-
Create a remix project with
@vercel/remix
@supabase/auth-helpers-remix
andtailwindcss
-
Create a route with a loader deferring results from supabase
-
Add cache control headers
-
Make changes to tailwind and save
-
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.
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.
@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
Yeah encountering same issue over here w prisma
Seeing this intermittently as well.
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
@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?
@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.
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
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
@brophdawg11 This was my initial solution but exposing the entry.server.tsx breaks the application when using Vercel.
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?
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.
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.
HEllo, i have the same issue ⬆️
with :
├── @remix-run/[email protected]
├── @remix-run/[email protected]
├── @remix-run/[email protected]
├── @remix-run/[email protected]
├── @remix-run/[email protected]
├── @remix-run/[email protected]
├── @tailwindcss/[email protected]
├── @types/[email protected]
├── @types/[email protected]
├── @types/[email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
We are facing same issue when defer fetches third party data, probably related to timeout as mentioned by some.
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>
);
}
I'm also experiencing the same issue with deferred data. I tried with higher ABORT_DELAY value but nothing changed.
this error is making the whole streaming feature redundant imho.
Remix 2.6.0 still has this issue
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 ?
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.
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
- If I set the ABORT_DELAY to 5000
const ABORT_DELAY = 5_000;
- and then create two routes like this:
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>
);
}
- and then navigate to '/uncool' I will get a timeout. No surprises there.
- 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.
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.
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:
- Aborting the
renderToPipeableStream
call - 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.
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:
- Aborting the
renderToPipeableStream
call- 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.
@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!
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/[email protected]
├── @remix-run/[email protected]
├── @remix-run/[email protected]
├── @remix-run/[email protected]
├── @remix-run/[email protected]
├── @remix-run/[email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
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.
Yes, the reload (and reload on save) does indeed cause an immediate timeout. Same here.
@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"
},
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
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(
<I18nextProvider i18n={instance}>
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>
</I18nextProvider>,
{
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(
<I18nextProvider i18n={instance}>
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>
</I18nextProvider>,
{
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);
});
}