Closed AndrewGlazier closed 3 years ago
I do not have time to work on this issue - outside contributions would be greatly appreciated.
@AndrewGlazier I have fixed this issue by adding the onSameUrlNavigation: 'ignore' option to router module in angular.
Oh wow thank you for sharing that @rknash18. It would be great if that workaround works for everyone else's issues they've been having, too. I've commented in #94 asking others to test if this works for them.
If it does, let's look into incorporating this into single-spa-angular so that all benefit from it.
Hi @joeldenning , @rknash18 ,
The solution provided in the comment does not work for me I m stilling using the changeDectection ref as an alternative.
@ashuaggarwal94 I am not sure about the issue you are experiencing , I have fixed issue with router trying go back on forth when I clicked on a menu option.
I identified that this loop starts in reroute.js file
In my case performAppChanges reached 400 times until the page was available again
Edit 1: Also find out that it happens because of replaceState in navigation-event.js file, which calls reroute.js above methods.
As you can see, it happens in the following order:
Default behavior:
The conflict occurs between following routes https://my-url/route-before-problem/ https://my-url/route-trying-to-access No other routes are involved in proccess (what I mean is that even though Angular makes redirects because of authorization it always happens between same two routes)
My current guess is this line. I think the second line
urlReroute(createPopStateEvent(state))
is runned after the second router change, causing unexpected behavior since browser is already changing again (to new url) and single-spa is processing the old url.
PS: JUST A GUESS, trying to debug it a little more.
Thanks for helping to debug this - why would single-spa's code have the old url? Angular router should call replaceState
with the new url, not the old one.
What I meant was that single-spa reroute is happening during an router change, which causes the bug, but not sure about it at all. It feels like single spa is processing during another route change.
Test it out for about 2 hours. I'm creating an repo with the bug! Notice that loop starts after "finishUpAndReturn" method is called twice, @joeldenning, any idea?
Created an repo that reproduces the bug @joeldenning
I am not going to look at this bug anytime soon. I do not use Angular for any projects ever and this bug is a hard one when I tried to fix it in the past. The only path forward for this to be fixed is community contribution.
Notice something that might be the bug!!!
this.router.navigateByUrl('threee').catch(res => {
this.router.navigateByUrl('one');
});
This image shows the arguments send to replaceState and to pushState.
provide: APP_BASE_HREF, useValue: '/test'
, making all urls for that app to have /test prefix (/test will be in activtyFunction of SingleSPA)PS: Even though I do not alter APP_BASE_HREF the bug keeps happening and Angular keeps sending "/two" other than "/threee"
PS 2: I'm thinking that I could fix the problem checking if url is the same as current url. But I don't know if it can cause any other problems. Any other suggestions please 🙏?
Notice that the problem is in the following line If I delete the following line, bug is fixed.
@TheMcMurder (git blamed this code). I just need to know why is this code there. Because I not sure what might happen if I just delete it. And what you suggest, should I try to get why is pendingPromises making angular start a loop or just do something to fix this code (still don't know what)?
Full disclosure: I don't use angular for any projects.
I don't recall specifically why that line of code exists, but this is a really complicated interaction between two different libraries: angular-routing, and single-spa. They both operate with different ideas about how routing is to be accomplished and who is in control of routing.
I'll try to take a look at the repo you created, but if I'm understanding correctly it's only consistently reproducible in IE which will make debugging extremely hard (I use linux as my daily driver). I wouldn't suggest deleting lines of code in single-spa as a solution. I know it's a slow response and I thank you for your continued patience.
@TheMcMurder Actually is totally reproducible in Chrome / Firefox (using Windows at least). The bug happens 100% in my repo using the following settings: Chrome v 79.0.3945.117 (64 bits) Windows 10 (64 bits) v 18363.592 or in Firefox v 72.0.1 (64-bits) Microsoft Edge v 44.18362.449.0 (Microsoft EdgeHTML v 18.18363) Blisk v 12.0.92.83 (Official Build) (32-bit)
IE: I didn't tried.
PS: I removed that line from single-spa and builded to test and works just fine
Hi, I can trigger the loop in my apps with Chrome and Ubuntu too.
Also can confirm this happens in several of our angular micro-apps, running both the latest chrome and firefox nightly. It doesn't happen on every navigation event, but when it does happen it will happen consistently for a particular one.
This is something that I'm investigating as well, I'll post here if I figure anything out.
the redirection infinite loop stills exist.any solutions?
I try all solutions above,but failed. so i spend one day to solve the problem.luckily,it is fixed for me. update file:src/navigation/navigation-events.js details as my fork: https://github.com/kisslove/single-spa/blob/master/src/navigation/navigation-events.js
@gubertcalixto I add ‘isNavGoOrBack‘ ,that just make a distinction between click back-forward in browser and normal route. because the popstate event will be triggered by doing a browser action such as a click on the back or forward button (or calling history.back() or history.forward() in JavaScript). Normal route will trigger history.pushState and history.replaceState,but go back in browser only trigger history.replaceState. the problem is history.pushState and history.replaceState both call urlReroute(createPopStateEvent(state)).obsolutely,it duplicates.
This issue only happens if user click on two different single spa link in a fraction of second. To fix this issue I disabled the links until any of the single page application is loading.
When any application loads via single-spa, it runs below events at start and end. start - single-spa:before-routing-event end - single-spa:routing-event In between there are other events also gets called.
@HostListener('window:single-spa:before-routing-event')
beforeRouting() {
const navbar: HTMLHtmlElement = document.querySelector('navbar');
const activeLink: HTMLHtmlElement = document.querySelector('.nav-link.active')
navbar.style.pointerEvents = 'none';
activeLink.style.classList.add('loading');
}
@HostListener('window:single-spa:routing-event')
afterRoutingEvent() {
setTimeout(_ => {
const navbar: HTMLHtmlElement = document.querySelector('navbar');
const activeLink: HTMLHtmlElement = document.querySelector('.nav-link.active')
navbar.style.pointerEvents = 'none';
activeLink.style.classList.remove('loading');
}, 500);
}
@kisslove maybe you could create an PR with your correction for single spa. The bugs that I know/heard about was:
PS: If you don't do it I probably will do in the next month (on vacation right now 😎)
@pankajparkar The cases where I see it is when the back button is pressed or when doing a single direct navigation from using the router (not triggered by user interaction). It's possible though that either of those are causing cascading navigation events (guards, redirects, etc), so the theory of "too many navigation's too quickly" is valid.
I'm using the fork by @kisslove and it appears to be working, I did notice one time where the redirects started for a couple of bounces and then resolved themselves quickly, but that was single time out of several. So far I haven't seen any other negative side-effects or regressions within my apps from it.
@gubertcalixto , @kylethebaker .Notice that my pre updates could lead to some display problems when it routes between multi angular micro-fronts. Now,my solution is that adding a setTimeout fn to solve the loop.it also appears to be working. only update here.
window.history.replaceState = function (state) {
const result = originalReplaceState.apply(this, arguments);
// delete the row:
//urlReroute(createPopStateEvent(state));
//update as follow:
setTimeout(() => {
urlReroute(createPopStateEvent(state));
}, 50);
return result;
};
@kisslove with the setTimeout it's worse for me, I get stuck in the loop but it recovers after a few seconds (so no longer infinite but still won't work for us). I've noticed with the original change that I still do a really quick back-and-forth redirect when it happens, it just stops early enough that it's not very noticeable.
I'm also digging around with the navigation trying to see if I can get to the bottom of it. It does seem like there might be some kind of timing issue at hand though.
Our application bundles are quite heavy, they take seconds to load, and when these bundles are downloading and bootstrapping the application, if user try to switches between applications, redirection get into infinite loop.
The solution I applied is based on my assumption seems to be working for me. It could have been wrong in this approach. Please suggest if there is better way to fix this issue.
@kylethebaker I will give a shot with @kisslove 's solution. Just curious to know, after you updating the single-spa code, how you're utilising this modified package. From forked git repository, or you have hosted this package somewhere else?
@pankajparkar We have a private npm package registry that we use for internal packages, I forked it and published there.
There might be a simpler way to do it, but I updated the package name to @our-namespace/single-spa
and also updated the typings.d.ts
to reflect this new name, then updated the consumer app's package.json and imports to use the new package.
@kylethebaker Thanks for sharing the way.
I'm afraid doing that, because in future single-spa
releases, I won't be receiving any updates, Every update has to done manually.
Perhaps releated - https://github.com/single-spa/single-spa/issues/438#issue-564495428
After a long time debugging, found out that, at least the bug that is in my repo, occurs due to this line. I still could not found the reason of the problem but may have some ideas / conclusions (I AM NOT 100% WHAT I'M SAYING):
if arguments[2] !== location.pathname
being arguments[2] equal to the redirect path.PS: The example in my repo is the following:
I debugged this a while ago and came to the same conclusion @gubertcalixto. My best guess is that Angular Router relies on all navigation events occurring via the router and within the same Zone. In single-spa, other applications sometimes cause navigation events which causes problems for Angular Router. Also, even within a single application the problem can occur because single-spa delays the popstate
event from firing to all the listeners on the page until it knows whether to unmount the applications in charge of those listeners or not. This hasn't been a problem for any framework except for Angular, and my best guess is that it is related to the popstate event occurring outside of the Zone or outside of the Angular Router instance.
^ This is all the information I have. It sounds like you've made a ton of progress. If you get to a point where you have a suggested fix, that would be amazing. This issue has plagued single-spa-angular for a long time.
From https://github.com/single-spa/single-spa/issues/438:
Could you please provide me some context on why do we override window event listener functions and capturing popstate and hashchange events and processing them. Or can you redirect me to any documentation around single spa architecture. It would be of great help if you provide this information.
Sure! Single-spa overrides window.addEventListener and window.removeEventListener to prevent single-spa applications that are about to be unmounted from receiving a PopStateEvent or HashChangeEvent. Since single-spa applications are only aware of the routes described by their activity functions, they would show a Not Found 404 page if we let the PopStateEvent / HashChangeEvent through to them before we unmount them.
In other words, single-spa needs to be the "first" handler of the popstate and hashchange events. And its handling of those events is asynchronous, since it calls bootstrap()
, mount()
, and unmount()
during its handling of those events. Since it needs to be (1) first, and (2) asynchronous, we withhold the routing events from all single-spa applications until it finishes. The patching of window.addEventListener
is the only way that I'm aware of to accomplish that.
Here are some links in the single-spa source code that might shed further light on how it's happening:
window.removeEventListener('popstate')
, which is what is needed for them not to show a Not Found 404 page once callCapturedEventListeners is invoked.It's worth noting that no other frameworks have issue with this approach - react, vue, and others all are fine with it. My guess for the last while is that ZoneJS / Angular Router are assuming a specific timing or zone for the popstate and hashchange events that does not always happen when single-spa withholds the event for a short time before firing the Angular Router popstate event handler.
@joeldenning @gubertcalixto we are facing the same issue. Is there any progress or are there ideas on how to get it fixed? :)
Hi @muhlba91 - as I've said above, I would appreciate outside contribution on this issue.
Angular router and ZoneJS are extremely complex (I've read their source code!) and have been difficult for me to debug. I personally do not use Angular very often and single-spa-angular is often a source of pain and frustration for me, since I find Angular's approach (with schematics, hidden webpack config, and Angular CLI) to be quite brittle. I often do not prioritize spending time on single-spa-angular simply because I do not enjoy it.
I recently quit my job to do open source + consulting full time. I may get to this issue at some point, but have many other things I'll likely do before shifting attention to this. I would gladly prioritize working on this issue if sponsored (Twitter @joelbdenning). Otherwise, I'll look at this issue eventually on a good day when I'm mentally prepared to spend several hours reading minified angular router code 😄
Yeah, I totally see your point here and I’m not deep into frontend development myself and prefer more lightweight frameworks like vue and debugging such code is not my favorite thing to spend my days with. 😄 (And kudos for reading through both libraries and your open answer is really appreciated! 👍)
Well, business needs and companies go for various reason for Angular which is why I ended up digging through that as well today while trying to debug the problem. 🙈
I’m willing to spend some time/days on trying to at least find a working „hack/workaround“ which could then, hopefully, also open possibilities for a fix. 😃 However, as you have said already, the way all of those libraries work together is quite complex and I was hoping for any direction or idea you have got where I could start to pin down the problem?
where I could start to pin down the problem?
See https://github.com/single-spa/single-spa-angular/issues/113#issuecomment-592981422 and https://github.com/single-spa/single-spa/releases/tag/v5.2.0
Also note that this is almost certainly related to https://github.com/single-spa/single-spa-angular/issues/94
I believe that this is fixed in https://github.com/single-spa/single-spa-angular/releases/tag/v3.3.1 and https://github.com/single-spa/single-spa-angular/releases/tag/v4.0.0. single-spa-angular@3 is for Angular 2-8, and single-spa-angular@4 is for Angular 9. See #94 for the explanation and links to PRs that fixed it.
If someone could confirm that this is fixed in 3.3.1 and 4.0.0, please let me know here so I can close this issue.
If someone could confirm that this is fixed in 3.3.1 and 4.0.0, please let me know here so I can close this issue.
I can confirm - Issue is fixed
Great! Big thanks to @arturovt for finding and fixing this
If someone could confirm that this is fixed in 3.3.1 and 4.0.0, please let me know here so I can close this issue.
I can confirm - Issue is fixed
Hi @white-collar . Did you confirm it in 3.3.1 or 4.0? I'm on the process of updating our Angular 8 micros to 3.3.1 and If I navigate too fast between two of them I still get the infinite loop.
I am seeing the infinite redirect bug after clicking the back button between two single-spa-angular 4.0 apps, using the latest single-spa 5.3.4. The bug does not appear if I use @kisslove 's fork of single-spa mentioned in this thread, which is what we've been using as a workaround for this bug for the past few months.
There are a lot of moving parts in my application, but I'll try to get a repo standing that concisely reproduces the issue.
More background on my app which is admittedly not the intended/recommended setup - rather than have single-spa manage the lifecycle of my angular apps, I have a root Angular application that loads other single-spa-angular apps at certain routes as parcels. I noticed that with single-spa-angular 4.0 there've been some changes to better isolate the zones when multiple angular apps are running together. namely the monkey patch of NgZone.isInAngularZone and injecting the zone into the SingleSpaPlatformLocation. I figured I should do the same thing in my root Angular app to try to fix this issue, unfortunately it doesn't seem to make a difference. Here is the bootstrap code from my root app for reference (much of which is lifted from single-spa-angular source):
const appIdentifier = 'myAppRoot';
NgZone.isInAngularZone = () => {
// @ts-ignore
return window.Zone.current._properties[appIdentifier] === true;
};
const module = platformBrowserDynamic(getSingleSpaExtraProviders())
.bootstrapModule(AppModule);
/// cast to any because SingleSpaPlatformLocation is not exported
const rootPlatformLocation = module.injector.get(PlatformLocation) as any;
const rootZone = module.injector.get(NgZone);
// tslint:disable-next-line:no-string-literal
rootZone['_inner']._properties[appIdentifier] = true;
rootPlatformLocation.setNgZone(rootZone);
I also have a situation of infinite URL, but with different code. recreated that situation with https://github.com/varora1406/coexisting-angular-microfrontends/commit/7073a82e237d47fbb809beb3e12a861c2d91c4d3.
Any helpful comment from community on it?
To solve this bug, we need Angular router to ignore the single-spa popstateevent if it was the app that triggered the navigation by calling pushState()
.
@arturovt and I have discussed this but haven't found a solution for it. We have both shifted our attention to other work right now, but would gladly accept a PR for anyone who has an idea of how to accomplish the fix described above.
Another thing - the single-spa core team is proposing a new dom event called statechange
that would better facilitate multiple SPA routers on the page at the same time. The root of this URL infinite redirect bug is Angular's handling of multiple routers on the same page, and that statechange event would provide a clearer path for Angular's code to handle this properly. The proposal can be found here - https://github.com/whatwg/html/issues/5562
^ Sharing that not because it will end up being the fix we use immediately, but just that we're trying to make progress on all fronts with this bug. Angular Router is a rather brittle router in that it expects no other code to ever trigger navigation events. However, we'll continue look for a workaround that.
Here's the proposed workaround:
Here's an idea of what the code might be for it:
class SingleSpaPlatformLocation extends ɵBrowserPlatformLocation {
private skipNextPopState: boolean;
pushState(state: any, title: string, url: string): void {
this.skipNextPopState = true
return super.pushState(state, title, url);
}
replaceState(state: any, title: string, url: string): void {
this.skipNextPopState = true
return super.replaceState(state, title, url);
}
onPopState(fn: (event: LocationChangeEvent) => void): void {
if (this.skipNextPopState) {
this.skipNextPopState = false;
} else {
super.onPopState(event => {
this.ngZone.run(() => fn(event));
})
}
}
}
Issue seems to relate to this comment; https://github.com/CanopyTax/single-spa-angular/issues/94#issuecomment-520550455 Creating a new ticket as I cannot reproduce using the browser back button.
This bug is reproducible in the vanilla example for coexisting angular micro-frontends found here: https://github.com/joeldenning/coexisting-angular-microfrontends
Reproduction steps; Using the navigation app, trigger a route change, and then quickly trigger another nav change.
Issue is reproducible on; IE11 (Very common) Firefox (Mediocre-Hard) Chrome (Rare)
Notes; In our application we have found that having more simultaneous angular apps running in parallel triggers the issue more commonly, however this bug is reproducible with only one app actively loaded by the single-spa (however with difficultly even in IE11). (Issue does not occur at all with the app running in angular standalone without the spa)
The issue appears to be also related to Router.forRoot in some way, upon removing this from an app, the issue becomes more difficult to reproduce even with IE11, a similar effect to whether the app wasn't running.
This seems to be related to the speed of the navigation. Navigating twice very quickly causes this effect.
If struggling to reproduce, we found that having a button which changes to swap between two pages and spam clicking it causes the bug to reproduce on IE11 every time.
The navigations do not need to be between separate apps, routing between two pages on the same app causes the issue.