DEV Community

Krzysztof Platis
Krzysztof Platis

Posted on • Edited on

Don’t use global static objects - avoid race condition in SSR Angular 🏎

Instead of using a global static object, you should create an injectable service. Otherwise in SSR server two apps bootstrapped for two concurrent http requests can change and read that object at the same time. It’s prone to race condition.

Sometimes it’s not so obvious we’re using a static global object, especially when we import it from a 3rd party library.

3rd party library i18next

In an Angular project I imported the translations engine from the 3rd party vanilla JS library: import i18next from ‘i18next’. Then in an APP_INITIALIZER I set the active language i18next.changeLanguage(urlLang) based on the URL param, for example site.com/en/ or site.com/jp/. Then this language was used behind the scenes to translate labels in the components, i.e. i18next.t(‘translation.key’).

Race condition

When the SSR server received 2 http requests in a row and handled them concurrently in one NodeJS process - site.com/en/ and site.com/jp/ - the global current language was set first to English, and then immediately overwritten to Japanese. The components of both apps were rendered a little later, so both were using Japanese for translating labels. Therefore a user requesting an URL site.com/en/ could get a response in Japanese, which was a bug.

Bugfix

To fix it I needed to create a fresh i18next instance i18next.createInstance() for each bootstrapped app. So I wrapped it in an injection token provided in the app’s root injector:

import i18next from i18next;

export const I18NEXT_INSTANCE = new InjectionToken('I18NEXT_INSTANCE', {
  providedIn: 'root',
  factory: () => i18next.createInstance(),
});
Enter fullscreen mode Exit fullscreen mode

Then this instance could be accessed only by the injectable classes (components, services, …) inside the same bootstrapped app:

constructor(
  /* ... */
  @Inject(I18NEXT_INSTANCE) protected i18next
) {}
Enter fullscreen mode Exit fullscreen mode

If you really feel like buying me a coffee

... then feel free to do it. Many thanks! 🙌

Buy Me A Coffee

Top comments (4)

Collapse
 
ayyash profile image
Ayyash

how did you test the two concurrent requests? I am concerned, I have multilingual apps running off nodejs, but I capture the language from the middleware then populate the global, is the global shared amongst all sessions served? how do I confirm?

Collapse
 
krisplatis profile image
Krzysztof Platis

I would do the following test:

  1. slow down artificially rendering of each requests, by adding setTimeout(()=>console.log(myGlobal.getCurrentLanguage())) to the logic
  2. send simultaneously 2 requests to my server, for 2 different languages. Then observe the console and see if 2 different langauges were logged, or the same one.

It all depends whether your global is a global in the scope of the whole NodeJS process, or is a global just in scope of your Request handler? (e.g. you can store the langauge in the Request's object metadata).

Collapse
 
armen96work profile image
armen96work

Is there a way to get Hostname with @angular/ssr?

Collapse
 
krisplatis profile image
Krzysztof Platis

You can retrieve the hostname from the ExpressJS Request object. It should be also accessible in the Angular app via REQUEST injection token (historically imported from @nguniversal/express-engine/tokens, but since ng17 removed, and now you need to provide it yourself to Angular if needed).

Note: If you're running SSR behind a reverse proxy, you might need to use X-Forwarded-Host http header, to retrieve the original requested hostname. But beware to use it only if the server that sent the X-Forwarder-Host is trusted (as this header might be spoofed). As an example, see the ExpressJS logic we implemented here + an ExpressJS config to trust 'loopback'. For more, see the official ExpressJS docs on "Express behind proxies"