Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Future <Link> navigation is broken if prefetching that route takes longer than 3.8 sec #28797

Closed
jayphelps opened this issue Sep 5, 2021 · 3 comments · Fixed by #28899
Closed

Comments

@jayphelps
Copy link
Contributor

jayphelps commented Sep 5, 2021

What version of Next.js are you using?

11.1.2/Canary

What version of Node.js are you using?

Any

What browser are you using?

Any

What operating system are you using?

Any

How are you deploying your application?

Any

Describe the Bug

The internal utility withFuture() sets internal state in the routes Map containing a Promise. When loading or prefetching, the internal loadRoute() uses the resolvePromiseWithTimeout (), which will reject after the given 3.8 second timeout.

If a route is navigated to for the first time without being prefetched, and the 3.8 timeout is triggered due to slow server/connection, an exception is thrown and ultimately the page will do a full navigation. However, if an attempt to prefetch the route is made by Next and it takes longer than 3.8 seconds, that exception is caught and swallowed in production mode. Because withFuture() does not listen for any rejections from the promise returned from generator() (or, unrelated, if generator() synchronously threw) the state stored in routes (passed into withFuture as the second argument ,map) is not cleaned up and the "future" promise stored in there will never be resolved nor rejected.

When induced, the symptom from the user is that clicking a <Link> does nothing visually, and no error is logged.

Expected Behavior

Preloading <Link>s that take longer than the timeout do not prevent future navigation to that route.

To Reproduce

To reproduce, clone/build/start this repo, but do NOT npm install because I've patched next to induce 5 second latency to the /about JS chunk only to make it easily reproducible: https://github.com/jayphelps/next-issue-28797

Alternatively, the general instructions for making your own:

  • <Link href="/something"> to another page (without setting prefetch={false}).
  • Some how induce latency over 3.8 seconds for only the JS /_next/static/chunks/pages/file.js for that particular page; it's much harder to reproduce if you induce general latency on everything, but consistently reproducible if only that one file.
  • Visit that page with that <Link>.
  • See in Network tab that JS resource is being prefetched, but taking longer than 3.8 seconds.
  • Wait until JS resource download completes.
  • Now click on the <Link>.
  • See that nothing happens.

To confirm root cause, add a breakpoint to the prefetch error swallowing line, reproduce (seeing debugger pause on that breakpoint), then inspect internal routes map to see the hung promise stuck in 'pending' state forever.

If things are unclear, I'm happy to jump on Zoom, etc.

As always, apologies if I'm some how wrong and waste anyone's time 😅 I tried to be thorough. I would PR, but it’s unclear if the right approach is a more extreme refactor or just simply tweaking withFuture to listen for errors and cleanup—that might have consequences I’m unaware of because it’s used in several places.

@jayphelps jayphelps added the bug Issue was opened via the bug report template. label Sep 5, 2021
jayphelps added a commit to jayphelps/next.js that referenced this issue Sep 5, 2021
@jayphelps
Copy link
Contributor Author

jayphelps commented Sep 5, 2021

This fixes it for us, and what we're now running in production: jayphelps@8daaa0a

-      generator().then((value) => (resolver(value), value))
+      generator()
+        .then((value) => (resolver(value), value))
+        .catch((e) => {
+          map.delete(key)
+          throw e
+        })

Though it's probably a good idea to catch any synchronous errors when calling generator() too (which we're not doing)

jayphelps added a commit to jayphelps/next-issue-28797 that referenced this issue Sep 5, 2021
@jayphelps
Copy link
Contributor Author

Edited original post to include a repo to reproduce the issue easily; I patched next-server.js in the node_modules (included) so that it always has 5 seconds of latency for the /about route chunk. https://github.com/jayphelps/next-issue-28797

@ijjk ijjk added kind: bug and removed bug Issue was opened via the bug report template. labels Sep 7, 2021
@kodiakhq kodiakhq bot closed this as completed in #28899 Sep 8, 2021
kodiakhq bot pushed a commit that referenced this issue Sep 8, 2021
This applies the fix from the awesome investigation done in #28797 by @jayphelps and adds a test to ensure this is working as expected. It seems that the `route-loader` has a race condition while prefetching and if a script is executed before we have created a current "future" entry to resolve the entry stays in a pending state causing routes to hang so this handles the condition by ensuring pending/errored entries do not stay around. 

## Bug

- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [x] Errors have helpful link attached, see `contributing.md`

Fixes: #28797
Fixes: #27783
@balazsorban44
Copy link
Member

This issue has been automatically locked due to no recent activity. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

@vercel vercel locked as resolved and limited conversation to collaborators Jan 27, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants