Wednesday, 22 December 2021

Angular ng-upgrade stuck in infinite loop with singleton provider

We're performing a AngularJS-Angular migration for a big client with a huge, messy codebase (794k+ JS LoC, 396k+ JSP LoC) and have hit an issue we're struggling to solve. It's a bit of a contrived issue so I'll try to explain the context a bit.

Until recently they had a couple dozen different manually copied versions of AngularJS, all 1.5.6 or lower. We managed to get them onto a single, NPM-managed copy of AngularJS 1.8.2. However, some of the breaking changes were so big that it was felt we couldn't afford to fix them everywhere- eg the $http breaking changes affected thousands and thousands of places across this codebase.

So, what we did instead was to backport the HTTP service (and a couple others) from 1.5.6 into 1.8.2. We use the .provider(...) function to pass a copy of the old provider into the new version of AngularJS. We only do this where there are no known security fixes in the services we're backporting. This solved the issue but much later down the line we encountered another problem: some providers like the HTTP provider have state. This state can be lost when the provider is used multiple times- AngularJS uses new to instantiate the provider from our constructor, at which point any previous state of the HTTP provider is wiped out. In the client's app there is a lot of complex misdirection so it is very possible for the same provider to get backported twice in the same session. Hence, we have an issue: the provider gets constructed twice and on the second construction wipes out the state of the HTTP provider that may have been changed before the second construction.

So, to avoid this happening (I feel a bit like I'm at a confession box here...) we added a layer of abstraction to turn it into a singleton:

let innerHttpProviderInstance = null;
function $HttpProvider_V_1_5_6() {
  if(innerHttpProviderInstance == null) {
    innerHttpProviderInstance = new $HttpProvider_V_1_5_6_inner();
  }
  return innerHttpProviderInstance;
}

//The original HttpProvider from 1.5.6
function $HttpProvider_V_1_5_6_inner() { ... }

Which is then used like so (in numerous places):

const app = angular.module('app', mainAppDependencies).config(['$provide', '$controllerProvider','$locationProvider', function($provide, $controllerProvider,$locationProvider) {
    ...
}])
  .provider('$http', $HttpProvider_V_1_5_6)

Now, we're finally close to having the upgrade to AngularJS 1.8.2 completed and are looking at migrating to Angular using ng-upgrade. We've got quite a neat hybrid architecture setup: the Angular application upgrades the root AngularJS node, which in turn downgrades Angular leaf nodes. We hope to upgrade a few neaf nodes to start with, then one parent of those nodes at a time until we have entire branches on Angular. This is largely based on Victor Savkin's "Upgrading Angular Applications". That seemed to work well, until the above singleton change was introduced. Now, whenever the application loads it will get stuck in an infinite loop reloading the page and adding !#%2F to the start of the URL. This seems similar to the following GitHub issue, though it was apparently fixed: https://github.com/angular/angular/issues/5271

When we remove the singleton from our provider backport, it works fine. When we reintroduce it in any fashion (we've tried numerous approaches), it breaks again. I think it has something to do with binding, and ng-upgrade trying to reload the page because it thinks the state has changed but I'm really not clear. So, this is the nest on gundarks we find ourselves in. Any suggestions for our next steps?

Edit: We happened to stumble across setuplocationsync and it seems this may be relevant to what's going on here. If I understand correctly it is supposed to solve a known bug in which Angular/AngularJS trigger one another's routing, causing them to loop. When we call this function in our setup it almost solves the problem- the page will eventually load (whereas before it reloaded indefinitely) but it still goes through a couple dozen iterations of the reload, and instead of just adding !#%2F to the URL it now repeats the full URL like so (where we're trying to reach the page /full-url/): /full-url/full-url/full-url/full-url/.../full-url?params=vals



from Angular ng-upgrade stuck in infinite loop with singleton provider

No comments:

Post a Comment