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

Expo vector icons are not loading up and is producing "3000ms timeout exceeded" loops on Brave Browser #12382

Closed
HeinDiez opened this issue Apr 1, 2021 · 41 comments · Fixed by #22954
Labels
bug Font Platform: web Using Expo in the browser

Comments

@HeinDiez
Copy link

HeinDiez commented Apr 1, 2021

Summary

Upon closer investigation, it has to do with node_module/fontfaceobserver not loading or failing to load from multiple requests resulting in Expo Vector Icons not showing up.

Managed or bare workflow? If you have ios/ or android/ directories in your project, the answer is bare!

bare

What platform(s) does this occur on?

Web

SDK Version (managed workflow only)

No response

Environment

Expo CLI 4.3.2 environment info:
System:
OS: Windows 10 10.0.19042
Binaries:
Node: 12.18.3 - C:\Program Files\nodejs\node.EXE
npm: 6.14.6 - C:\Program Files\nodejs\npm.CMD
IDEs:
Android Studio: Version 4.1.0.0 AI-201.8743.12.41.6858069
npmPackages:
expo: ~40.0.0 => 40.0.1
react: 16.13.1 => 16.13.1
react-dom: 16.13.1 => 16.13.1
react-native: ^0.64.0 => 0.64.0
react-native-web: ^0.15.0 => 0.15.0
Expo Workflow: bare

Reproducible demo or steps to reproduce from a blank project

Refreshing the web browser will either Load the icon or produce an "Error: 3000ms timeout exceeded" loop with a bunch of requests from bundle.js.map. Most likely it will produce the error.

I think it is the same issue with #5935. I am using Brave Browser in debugging.

all Expo Vector Icon would either show up or not at all.

@HeinDiez HeinDiez added the needs validation Issue needs to be validated label Apr 1, 2021
@HeinDiez
Copy link
Author

HeinDiez commented Apr 3, 2021

Heads up!. I tried testing on multiple browsers and it also occurred on Chrome.

@SleeplessByte
Copy link

It happens on all browsers, and is not limited to Bare: #5961 (comment)

@brentvatne
Copy link
Member

it sounds like an issue with expo-font on web, anyone willing to look into this?

@SleeplessByte
Copy link

SleeplessByte commented Apr 4, 2021

I can confirm it's expo-font on web. The issue is seems to be here:

https://github.com/expo/expo/pull/5961/files#diff-8ab49ff6ac8063c577850a893dc685a318c5f9a6cbb927e7dba0986b47a19766R26

(Here on master: https://github.com/expo/expo/blob/master/packages/expo-font/src/ExpoFontLoader.web.ts#L92)

  • When it fails to load using FontFaceObserver, the entire font won't "continue" loading and will never show up, even if it were to load in the background.
  • The .load returns a promise that isn't "caught" by default, leading to an uncaught exception. It shouldn't lead to loops, but my guess is that either the user's code (HeinDiez) or brave is somehow retrying the function that starts loading the font.

If the fonts take more than 3 seconds to load Font Face Observer will consider them failed. You can increase this time by passing a larger value (e.g. 5000 for 5 seconds) as the second parameter to the check function.

There are various solutions, one being as follows:

  1. By default, allow for more time to load the font. I've had quite a lot of instances where the font takes 5+ seconds to load. This can be accomplished by doing .load(undefined, LONGER_TIMEOUT). If this is not wanted by default, make it a setting / parameter (but that's a bit weird in the tri-platform).
  2. If the font fails to load in x milliseconds, retry several times with exponentially increasing timeouts. Why not increase the original timeout to a large number? There are timeouts that are caused because of shitty connections, and those will resolve a second or third time. I haven't tested this, but this MIGHT require removing the style from the head and re-adding it, in order to trigger a network request. I have no clue if this is true.
  3. Add to the documentation that loading fonts may fail, and users can solve it by catching the rejected promise and retrying. (So yes, point 2 can be done in user land, but point 1 can not).

Why it breaks the icon if the font fails to load, I don't understand. The style should be added to the head, so I don't get why it breaks.

@HeinDiez
Copy link
Author

HeinDiez commented Apr 7, 2021

I managed to fix it by declaring the font on my assets folder and run it as a custom font instead.

async loadFonts() { await Font.loadAsync({ MaterialCommunityIcons: require('../../assets/fonts/MaterialCommunityIcons.ttf')}); this.setState({ fontsLoaded: true }); }

It somehow fix the problem but in rare occasion the error still occurs.

@SleeplessByte
Copy link

@HeinDiez that doesn't fix the underlying problem, and as you said you'll still see the issue.

@byCedric byCedric added Font bug Platform: web Using Expo in the browser and removed needs validation Issue needs to be validated labels Apr 14, 2021
@spencerlevitt
Copy link

Any update on this? I'm not positive it's because of vector-icons, but I also get the 3000ms timeout exceeded error due to apply(fontfaceobserver/fontfaceobserver.standalone) very randomly and have been trying to find a fix! @byCedric

@SleeplessByte
Copy link

It's always because of a font not loading within the allotted time and @expo/vector-icons loads fonts.

@spencerlevitt
Copy link

On expo for web, I am importing and waiting for my fonts until loaded == true:

import {
  MaterialIcons,
  Entypo,
  FontAwesome,
  FontAwesome5,
  AntDesign,
  Ionicons,
} from "@expo/vector-icons";
import Archivo_Expanded_Black from "./assets/fonts/Archivo_Expanded-Black.ttf";
import Archivo_Expanded_Bold from "./assets/fonts/Archivo_Expanded-Bold.ttf";
import Archivo_Expanded_Regular from "./assets/fonts/Archivo_Expanded-Regular.ttf";
import Archivo_Black from "./assets/fonts/Archivo-Black.ttf";
import Archivo_Bold from "./assets/fonts/Archivo-Bold.ttf";
import Archivo_Medium from "./assets/fonts/Archivo-Bold.ttf";
import Archivo_Regular from "./assets/fonts/Archivo-Regular.ttf";
import Archivo_SemiBold from "./assets/fonts/Archivo-SemiBold.ttf";

let [loaded, error] = useFonts({
    "Archivo-Expanded-Black": {
      uri: Archivo_Expanded_Black,
      fontDisplay: Font.FontDisplay.SWAP,
    },
    "Archivo-Expanded-Bold": {
      uri: Archivo_Expanded_Bold,
      fontDisplay: Font.FontDisplay.SWAP,
    },
    "Archivo-Expanded-Regular": {
      uri: Archivo_Expanded_Regular,
      fontDisplay: Font.FontDisplay.SWAP,
    },
    "Archivo-Black": {
      uri: Archivo_Black,
      fontDisplay: Font.FontDisplay.SWAP,
    },
    "Archivo-Bold": {
      uri: Archivo_Bold,
      fontDisplay: Font.FontDisplay.SWAP,
    },
    "Archivo-Medium": {
      uri: Archivo_Medium,
      fontDisplay: Font.FontDisplay.SWAP,
    },
    "Archivo-Regular": {
      uri: Archivo_Regular,
      fontDisplay: Font.FontDisplay.SWAP,
    },
    "Archivo-SemiBold": {
      uri: Archivo_SemiBold,
      fontDisplay: Font.FontDisplay.SWAP,
    },
    ...MaterialIcons.font,
    ...Entypo.font,
    ...FontAwesome.font,
    ...FontAwesome5.font,
    ...AntDesign.font,
    ...Ionicons.font,
  });

Am I doing something wrong here? @SleeplessByte

@SleeplessByte
Copy link

SleeplessByte commented Apr 24, 2021

If you read my previous comment (#12382 (comment)) you can see that the issue is actually inside expo-font and there is nothing you can do to fix it, other than not using expo-font to load the font, but construct FontObserver with a higher timeout and handle the error cases.

That said, you are loading 14 fonts, which seems very excessive.

@spencerlevitt
Copy link

I can definitely cut down on the fonts, but I thought loading all of the vector-icons used throughout the web app upfront might help the issue?

Do you know of any way I can simulate/trigger the issue in dev so I can see what the user experiences when this happens? I've never gotten the error myself, I just see it being tracked on Sentry. Thanks for all the help. @SleeplessByte

@SleeplessByte
Copy link

The easiest way is to turn on throttle in network (in developer tools) so at least one font takes longer to load than the default timeout of 3000ms.

@spencerlevitt
Copy link

Do you know of any fix / hacky workaround to make sure the site doesn't break when this happens?

@HeinDiez
Copy link
Author

there is nothing you can do to fix it, other than not using expo-font to load the font,

Hi @SleeplessByte, is there a way to not use expo-font to load a font? I haven't seen any documentation for it, or maybe I just missed it somewhere.

@SleeplessByte
Copy link

SleeplessByte commented Apr 29, 2021

Yes, all expo-font does for the web is:

  1. Create a font declaration for css,
  2. Add it to the stylesheet,
  3. Load the font using FontFaceObserver if it's available,
loadAsync(fontFamily: string, resource: FontResource)
const fontStyle = `@font-face {
  font-family: ${fontFamily};
  src: url(${resource.uri});
}`;

That fontStyle is added to the first available stylesheet, but it's about as performant to create a new style element and add it to the head if it's not found:

const ID = 'expo-generated-fonts';
const style: HTMLStyleElement = document.getElementById(ID) || 
  document.createElement('style')

// Add the font style to the <style> element
style.appendChild(document.createTextNode(fontStyle));

// Ensure the <style> element is in the head. This will keep it in the head
// if it already is there.
document.head!.appendChild(style);

If you want to wait for it to be loaded, you can use FontFaceObserver from https://fontfaceobserver.com/:

new FontObserver(fontFamilyName).load()

This defaults to the aforementioned 3 seconds. It's not a bad idea to use and a longer timeout, and retrying when it fails:

function reallyLoadFont(fontFamilyName, timeout = 3000 * 3) {
  return new FontObserver(fontFamilyName)
    .load(undefined, timeout)
    .catch(() => {
	  // oh - no
      return reallyLoadFont(fontFamilyName, timeout * 2)
    })
}

Naturally, you might want to limit the maximum timeout, or amount of times you retry, but this should get you started.

@HeinDiez
Copy link
Author

HeinDiez commented May 12, 2021

@SleeplessByte Hey, I totally get it... however is it ok to change a node module package file?... our project is currently run by multiple devs, and updating their side of the code would be such a hassle.

the change should be done in this file right?
https://github.com/expo/expo/pull/5961/files#diff-8ab49ff6ac8063c577850a893dc685a318c5f9a6cbb927e7dba0986b47a19766R26

@SleeplessByte
Copy link

I've used patch-package to include a diff into the repo of my own projects so other developers don't need to do anything.

@github-actions
Copy link
Contributor

This issue is stale because it has been open for 60 days with no activity. If there is no activity in the next 7 days, the issue will be closed.

@github-actions github-actions bot added the stale label Jan 27, 2022
@SleeplessByte
Copy link

Not stale, still relevant.

@github-actions github-actions bot removed the stale label Jan 27, 2022
@github-actions
Copy link
Contributor

This issue is stale because it has been open for 60 days with no activity. If there is no activity in the next 7 days, the issue will be closed.

@github-actions github-actions bot added the stale label Apr 27, 2022
@SleeplessByte
Copy link

Not stale, still relevant.

@github-actions github-actions bot removed the stale label Apr 28, 2022
@noeljackson
Copy link

Not stale, still relevant.

@kav
Copy link

kav commented Jul 11, 2022

Any interest in a PR here? Happy to take a stab at it as we are definitely seeing the same

@kav
Copy link

kav commented Jul 12, 2022

Might be worth replacing with https://developer.mozilla.org/en-US/docs/Web/API/FontFace/load at this point

@github-actions
Copy link
Contributor

This issue is stale because it has been open for 60 days with no activity. If there is no activity in the next 7 days, the issue will be closed.

@github-actions github-actions bot added the stale label Oct 11, 2022
@SleeplessByte
Copy link

Issue still present

@github-actions github-actions bot removed the stale label Oct 11, 2022
@HeinDiez
Copy link
Author

Issue still present

Thanks @SleeplessByte for giving this an attention. Hopefully we get a fix soon. Maybe I'll look into it and see if I could find a fix.

@findhumane
Copy link

I just hit this issue where icons intermittently fail to load and it's much stranger than just a simple server-side or network slow-down. Below is a Wireshark capture from the client. The browser (192.168.1.251) sends a request for /fonts/MaterialCommunityIcons.ttf at 03:05:34.986 (frame 1488) and then, about 4 milliseconds later, the browser sends a TLS Close Notify (frame 1489) and then tears down its half of the socket with a FIN/ACK (frame 1490). Meanwhile, the server (143.198.245.8), in parallel, dutifully responds with a successful HTTP 304 about 40 milliseconds later (frame 1500). In the Firefox network inspector, this request shows as "No response" so it seems this request was canceled by the browser for some reason. Presumably, since the client had already given up on the request, it sends a RST packet for good measure to make sure the server knows it's not interested in this socket anymore (frame 1502).

1488	2022-12-24 03:05:34.986449191	192.168.1.251	143.198.245.8	HTTP	635	53	GET /fonts/MaterialCommunityIcons.ttf HTTP/1.1 
1489	2022-12-24 03:05:34.990468743	192.168.1.251	143.198.245.8	TLSv1.3	90	53	Alert (Level: Warning, Description: Close Notify)
1490	2022-12-24 03:05:34.990509057	192.168.1.251	143.198.245.8	TCP	66	53	56836 → 443 [FIN, ACK] Seq=2779 Ack=5930 Win=64128 Len=0 TSval=629929637 TSecr=4263784990
1500	2022-12-24 03:05:35.032238954	143.198.245.8	192.168.1.251	HTTP	247	53	HTTP/1.1 304 Not Modified 
1502	2022-12-24 03:05:35.032277292	192.168.1.251	143.198.245.8	TCP	54	53	56836 → 443 [RST] Seq=2755 Win=0 Len=0

All of this happens within dozens of milliseconds.

My code right before the call to Font.loadAsync prints a timestamp:

[2022-12-24T03:05:35.078Z] Loading fonts

So the font loading started only about 800ms before the request for that font. So I don't see any reason why the client should have canceled the request. This is well before the 3000ms timeout error which happens about 2.5 seconds later:

[2022-12-24T03:05:38.200Z] Error loading fonts AppUtilities.js:50:12
Error: 3000ms timeout exceeded

So the first issue is that the font loader seems to be mysteriously canceling its own request for no obvious reason well before the timeout and with the server trying to send a successful response.

At first, I was thinking might be due to scheduling delays, but the seeming canceling of the request well before the timeout seems like a functional issue rather than a timing or scheduling issue.

Then, there is another request for MaterialCommunityIcons.ttf, although I'm not sure where this is coming from. There are other "Error: 3000ms timeout exceeded" console messages (with characteristically ambiguous stack traces) although from "Uncaught (in promise)" which means it's not from my direct Font.loadAsync call but must be coming from somewhere else.

In any case, that call doesn't have the TLS Close or RST and succeeds quickly:

2066	2022-12-24 03:05:36.664555172	192.168.1.251	143.198.245.8	HTTP	635	61	GET /fonts/MaterialCommunityIcons.ttf HTTP/1.1 
2090	2022-12-24 03:05:36.711881821	143.198.245.8	192.168.1.251	HTTP	247	61	HTTP/1.1 304 Not Modified 

I also see the Firefox network panel showing this second request and this time it looks like a healthy response.

On the server-side, everything looks normal for both requests:

[24/Dec/2022:03:05:36.271] 304 163 499 2/2/0/0/0 0/0 "GET /fonts/MaterialCommunityIcons.ttf HTTP/1.1"
[24/Dec/2022:03:05:37.951] 304 163 499 4/4/0/0/0 0/0 "GET /fonts/MaterialCommunityIcons.ttf HTTP/1.1"

Nevertheless, those icons never did load in my app, so the second issue is that a successful loading of the icons may still not show them in the app.

I'll add some retry loops but I'm worried this will be fruitless because even a successful response didn't load the icons.

I think this also exposes the long-standing, systemic issue in Expo (and the JS community in general) that these sorts of issues are incredibly hard to create a standalone reproduction for and might depend on various external factors. This might be a good use case to explore alternative diagnostic capabilities of dynamically-enabled debug logging statements that us users of Expo can enable in our apps to help investigate these sorts of issues (e.g. a log statement when the font loader decides to cancel the request, etc.).

@findhumane
Copy link

After hours of fruitless investigation, I've decided to use a workaround of just extracting out the font icons I need into images and using those instead. It's cumbersome and inflexible, but I also noticed that MaterialCommunityIcons.ttf was 1.1MB, so I get the side benefit of less network consumption.

  1. Find icon on https://icons.expo.fyi/
  2. Since the font icon isn't selectable text, inspect it in the browser dev tools and then copy the text
  3. Create the PNG from the command line, pasting the clipboard after label: and changing the color with -fill:
    convert -background none -fill "#007AFF" -font .../MaterialCommunityIcons.ttf -pointsize 24 label:󰈳 filter-outline-blue.png
    

@jeltehomminga
Copy link

Also seeing this issue. unfortunately vector icons not working on web

@SleeplessByte
Copy link

SleeplessByte commented Feb 22, 2023

@findhumane you can find all the icons (as SVG and PNG) on http://materialdesignicons.com/, because that's what the Material Community Icons font is.

I also read your investigation. It's really not that complex, but I can support your findings with source code, as I've posted before (in this thread).

The package used is fontfaceobserver, which is imported here:

https://github.com/expo/expo/blob/master/packages/expo-font/src/ExpoFontLoader.web.ts#LL2C27-L2C43

...and called here:

https://github.com/expo/expo/blob/master/packages/expo-font/src/ExpoFontLoader.web.ts#L92

As you can see no arguments are passed. In this case, no timeout is given and it will use the default timeout of 3 seconds, as declared here:

https://github.com/bramstein/fontfaceobserver/blob/master/src/observer.js#LL195C51-L195C51

After 3 seconds, it aborts the request.

That's really it.

You can find previous explorations of the code base here:
#12382 (comment)

And the fix:
#12382 (comment)


I've been using a patch to retry automatically for the past 2 years and that works well. I recommend you do the same.

@findhumane
Copy link

@SleeplessByte

Thanks, great info!

After 3 seconds, it aborts the request.

That's really it.

Right, the strange thing though is:

The browser (192.168.1.251) sends a request for /fonts/MaterialCommunityIcons.ttf at 03:05:34.986 (frame 1488) and then, about 4 milliseconds later, the browser sends a TLS Close Notify (frame 1489) and then tears down its half of the socket with a FIN/ACK (frame 1490). Meanwhile, the server (143.198.245.8), in parallel, dutifully responds with a successful HTTP 304 about 40 milliseconds later (frame 1500). In the Firefox network inspector, this request shows as "No response" so it seems this request was canceled by the browser for some reason.
[...]
So the font loading started only about 800ms before the request for that font. So I don't see any reason why the client should have canceled the request. This is well before the 3000ms timeout error which happens about 2.5 seconds later.
[...]
So the first issue is that the font loader seems to be mysteriously canceling its own request for no obvious reason well before the timeout and with the server trying to send a successful response.

At first, I was thinking might be due to scheduling delays, but the seeming canceling of the request well before the timeout seems like a functional issue rather than a timing or scheduling issue.

I'm not a fan of introducing arbitrary delays and retries so I will stick to images until this is fixed. I like the idea of switching to FontFace.load.

Thanks for the icon images link, very useful!

@HelioDantas
Copy link

HelioDantas commented May 13, 2023

We can include the timeout property in the FontResource type and pass it to the FontObserver as follows:
return new FontObserver(fontFamilyName, { display: resource.display}).load(null, resource.timeout);

"What do you think?"

@findhumane
Copy link

findhumane commented May 14, 2023

We can include the timeout property in the FontResource type and pass it to the FontObserver as follows: return new FontObserver(fontFamilyName, { display: resource.display}).load(null, resource.timeout);

"What do you think?"

As evidenced in a previous comment, this issue is beyond a server-side timeout issue. Moreover, retries and timeouts are not a proper solution for this; even for the cases where they may be a workaround, they simply add latency for the user.

Given how hard this issue is to reproduce (and I've long moved on and now simply use images instead), I think the better solution is to switch to FontFace.load as mentioned in a previous comment: https://developer.mozilla.org/en-US/docs/Web/API/FontFace/load

Hopefully that will avoid whatever bug lurks in fontfaceobserver (or in browsers driven by its interaction).

An even cooler solution would be to automagically replace icons with SVGs from the font, thus avoiding loading the entire font TTF/OTF files which are huge (MBs) and most people are just using a few characters from each font, so they're a heavy waste on web.

@nandorojo
Copy link
Contributor

nandorojo commented May 14, 2023

My two cents as someone who has faced this and used expo vector icons: if you aren't deep into using @expo/vector-icons, I recommend not using it if you want to target Web. Instead, use a package that uses react-native-svg-based icons under the hood.

I've faced so many issues with this package on Web, so I thought I'd share. It isn't necessary for an icon set to be a single font. The SVG approach works great and doesn't have any of the issues with loaders and assets.

If you want to use any of the existing icon sets, you can use SVGr to convert a folder of SVGs into a React Native icon package.

I turned Hero Icons and Iconic into React Native SVG packages with this approach. You can see the script I used in those repos to generate your own icon packs if you'd like. It's easy to fork and implement.

I know it isn't a solution to this thread, but figured I'd save someone else the time. (I'm in too deep to remove Expo's icons, but I get OP's error everyday on our Sentry on Web, and I wish I could get rid of the package.)

@rootedsoftware
Copy link
Contributor

I'm having this same issue when I try to use icons in React Native on Android (expo)

EvanBacon pushed a commit that referenced this issue Jul 3, 2023
# Why

Fixes #12382

# How

The font observer can time out and throw an uncaught exception, as noted
by many bug reporters. There are many cases (e.g., unsupported browsers)
where the observer is early-returned, so catching an exception on web is
a safe no-op.

# Test Plan

This is tough to write a new test for since this is mostly a product of
network conditions; does this need new test coverage?

# Checklist

- `[N/A]` Documentation is up to date to reflect these changes (eg:
https://docs.expo.dev and README.md).
- `[N/A]` Conforms with the [Documentation Writing Style
Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md)
- [x] This diff will work correctly for `npx expo prebuild` & EAS Build
(eg: updated a module plugin).
@findhumane
Copy link

Appreciate the work to (hopefully) resolve this, although I think it's worth highlighting a previous, buried comment:

I also noticed that MaterialCommunityIcons.ttf was 1.1MB, so I get the side benefit of less network consumption

I think an even better long-term solution here would be to have some sort of asset pipeline that sees the icons being used and automatically replaces them with SVGs in the final build.

@expo-bot
Copy link
Collaborator

Some changes in the following packages that may fix this issue have just been published to npm under next tag 🚀

📦 Package 🔢 Version ↖️ Pull requests 📝 Release notes
expo-font 11.5.0 #22954 CHANGELOG.md

If you're using bare workflow you can upgrade them right away. We kindly ask you for some feedback—even if it works 🙏

They will become available in managed workflow with the next SDK release 👀

Happy Coding! 🎉

@JPStrydom
Copy link

Not sure if this is the correct place to raise this, but Expo SDK 49.0.21 currently still recommends expo-font 11.4.0, which doesn't include this fix. This can be checked by running expo install --check, which will complain wen a library is incompatible with the current version of Expo. This check says the following:
image

Is there any way we could get the Expo SDK to include this fixed version?

@JPStrydom
Copy link

Does Expo-Font 11.5.0 have any compatibility issues with Expo 49?

@JPStrydom
Copy link

Looks like upgrading to expo-font 11.5.0 or higher causes icons to break on native (rendering incorrect icons), but upgrading seems fine on expo web.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Font Platform: web Using Expo in the browser
Projects
None yet
Development

Successfully merging a pull request may close this issue.