Discourse not loading on legacy browsers

replaceAll is unsupported on iOS <= 13.3

This PR will add detection for this feature, and fall back to the basic HTML view if needed:


3 posts were merged into an existing topic: Frontend on Prod Site Down – Ember/Test error

Thanks @Ian_W for the report :+1:

We’ve now fixed the issue. This topic will close in a day.


I can report only partial success on legacy Firefox on Android.

On both meta.discourse.org and community.jenkins.io I now see the “browser is too old” banner", same as on the WINXP PC running FF 52.9.0 ESR.

But there no topics, only the logo and the footer [Home … Privacy Policy].

On Jenkins site, I do see the Categories, categories description and “Topics” count, but clicking on any category, I again can’t see any issues.


1 Like

I got meta.discourse.org to render the old HTML version in Firefox 40:

This should happen on all Firefox < 77.


As noted, my screenshot comes from the Android variant. Site seems to work fine on a Desktop variant.

I have no idea how I can extract additional information from the FF Android browser to assist debug.


Just installed the deprecated fennec Firefox 68.9 on my phone, and it appears to be working now:

Any ideas what I’m missing while trying to reproduce the bug?


Well, I’m not sure what to say. I just took a factory reset Android phone, (running 7.0 'cos it’s an old phone), downloaded Firefox Android 68.11 APK, installed and navigated to meta(.)discourse(.)org ** and get the prior screenshot.

Then it gets weird. Goto meta(.)discourse(.)org/t/ ** and I get:

"Oops! That page doesn’t exist or is private.

But then I get a list of “Popular” and “Recent” topics and can see this very topic.
Clicking on the topic, I new see the topic title, plus “bug”, “pri-high” but no content.

**: 2-link limited post
(also can’t seem to upload screenshots via UI, so had to upload by reply, one at a time)

1 Like

1 Like

Watching just in case someone comes up with a solution for using discourse on an iPhone 6 before I end up buying a new phone.

1 Like

I’m having the same issue, as explained below:

1 Like

For iOS < 13 or deprecated Firefox Fennec on Android, we should be showing the HTML view.

However, we are failing to do that because there is no HTML view due to

which is in our code base since forever.

Including the crawler content so we can fallback to HTML view is expensive, so maybe we want to add old Safari to

so it’s done only when necessary?

The other alternative being telling Babel to transpile replaceAll / finding a good enough polyfill.

cc @sam @david @gerhard


Other alternatives I can think of:

  1. Eliminate browser_update_user_agents altogether, use an IE6 compliant XHR request to get the content for rendering on these browsers on mobile. (disable javascript is already bust on mobile anyway)

  2. Polyfill replaceAll

  3. Include crawler content unconditionally (on mobile as well)

  4. Same as (1) but leave the setting as a micro optimisation, fallback to XHR.

I am swaying somewhere between (1) and (3), (4)


3 is something I’m afraid we will have to do eventuality, as the number of mobile devices abandoned by their manufacturers will only increase over time. It will increase some server load filling the template, and enlarge our HTML size on mobile but IMO we should do it.

So I’d say we do 3 and investigate if doing 1 and 2 is feasible.


We could flip stuff on its head, only enable the mobile optimisation on specific browsers, will let us still have this optimisation on 95% of the mobile traffic that hits us and still be very safe?


What about this @david

if (!String.prototype.replaceAll) {
	String.prototype.replaceAll = function(str, newStr){

		// If a regex pattern
		if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') {
			return this.replace(str, newStr);

		// If a string
		return this.split(str).join(newStr);


I changed the string branch of the solution you shared, so it fixes converting Strings to Regex without escaping.


I don’t think that will work when a function is passed as the second argument:

"my string with my example".replaceAll("my", (match) => `test${match}`)

(docs on the function can be found here)