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(),
});
Then this instance could be accessed only by the injectable classes (components, services, …) inside the same bootstrapped app:
constructor(
/* ... */
@Inject(I18NEXT_INSTANCE) protected i18next
) {}
If you really feel like buying me a coffee
... then feel free to do it. Many thanks! 🙌
Top comments (4)
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?
I would do the following test:
setTimeout(()=>console.log(myGlobal.getCurrentLanguage()))
to the logicIt 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).Is there a way to get Hostname with @angular/ssr?
You can retrieve the
hostname
from the ExpressJS Request object. It should be also accessible in the Angular app viaREQUEST
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 theX-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"