angular/universal

TransferState object is empty when TransferHttpCacheModule's interceptor is called clientside

front-a-little opened this issue ยท 8 comments

๐Ÿž Bug report

What modules are related to this issue?

  • builders
  • common
  • express-engine

Is this a regression?

Apparently since the last change in the TransferHttpCacheModule docs was Oct'22

Description

I've set up an Angular project using the CLI, added angular/universal and the TransferHttpCacheModule as described in their Readme. I've omitted the second step in main.ts since the newest Angular version puts bootstrapping after the DOMContentLoaded event OOTB.

I can see the serverside rendering working and I can see the transmitted serverstate in the delivered HTML document. Setting breakpoints clientside in the interceptors from TransferHttpCacheModule, I can see them kicking in, however, the injected transferState object is initialized but empty so the client can't match any of its requests to the registry

๐Ÿ”ฌ Minimal Reproduction

npm install -g @angular/cli
ng new my-app
cd my-app
ng add @nguniversal/express-engine
npm install @nguniversal/common --save

Add TransferHttpCacheModule to the app's main module as described in the aforementioned readme (don't do the main.ts step, bootstrapping already waits for DOMContentLoaded)

Add HttpClientModule and use HttpClient in any component ngOnInit() to fetch some data e.g. from a JSON file in /assets/

npm run dev:ssr

(Optional) on the client, set a breakpoint in the constructor of TransferHttpCacheInterceptor in common.mjs to see how the injected transferState object is empty.

Expected:
Call to JSON file (or endpoint) is resolved by node.js serverside, serverState is transfered via HTML document, client SPA doesn't resolve the call a second time after hydration.

Actual:
Call to JSON file (or endpoint) is resolved by node.js serverside, serverState is transfered via HTML document, client calls endpoint a second time since the interceptor runs before transferState is set somehow.

๐ŸŒ Your Environment


Angular CLI: 15.1.4
Node: 16.16.0
Package Manager: npm 8.11.0
OS: linux x64

Angular: 15.1.3
... animations, cdk, common, compiler, compiler-cli, core, forms
... material, platform-browser, platform-browser-dynamic
... platform-server, router

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1501.4
@angular-devkit/build-angular   15.1.4
@angular-devkit/core            15.1.4
@angular-devkit/schematics      15.1.4
@angular/cli                    15.1.4
@nguniversal/builders           15.1.0
@nguniversal/common             15.2.0
@nguniversal/express-engine     15.1.0
@schematics/angular             15.1.4
rxjs                            7.8.0
typescript                      4.9.5

Are you getting any errors on the server? Are you providing the full URI to the HttpClient when using doing the HTTP call?

Example

    this.http
      .get('http://localhost:37965/assets/a.json')
      .subscribe((data) => console.log(data));

Or

    this.http
      .get('/assets/a.json')
      .subscribe((data) => console.warn(data));

No errors on the server.
I'm aware of the absolute URLs problem in universal so I tried both adding localhost to the get() call as well as setting the server config to use absolute URLs

export class AppServerModule {
  // https://github.com/angular/universal/issues/1826
  constructor(@Inject(INITIAL_CONFIG) private config: PlatformConfig) {
    this.config.baseUrl = "http://localhost:4200";
    this.config.useAbsoluteUrl = true;
  }
}

But both ways have the same result. And even if the URLs were different on client and server state, it never has a chance to get matched since the transferState object is always empty

Hi @front-a-little, I am unable to replicate the issue with the steps provided.
Screenshot 2023-03-16 at 15 41 42

Can confirm, created a new minimal project and I get to the same state. I've compared the state transfer bit of both projects and they put the state script element in different places.

This is the markup for the faulty project:

        </app-root>
        <script id="serverApp-state" type="application/json">
            {&q;G.json.http://localhost:4200/assets/mockData.json?&q;:{&q;body&q;:[{ ...
        </script>
        <script src="runtime.js" type="module"></script>
        <script src="polyfills.js" type="module"></script>
        <script src="vendor.js" type="module"></script>
        <script src="main.js" type="module"></script>
    </body>

And this is the markup for the working project:

		</app-root>
        <script src="runtime.js" type="module"></script>
        <script src="polyfills.js" type="module"></script>
        <script src="vendor.js" type="module"></script>
        <script src="main.js" type="module"></script>
        <script id="serverApp-state" type="application/json">
            {&q;G.json.http://localhost:4200/assets/mockData.json?&q;:{&q;body&q;:[{ ...
        </script>
    </body>

Could this be caused by routing ? Frankly that's the only difference between the two projects, I skipped the routing module during ng new on the second one.

In my faulty project, this is the routing app module:

    import { NgModule } from '@angular/core';
    import { RouterModule, Routes } from '@angular/router';
    import {TeaserListComponent} from "./teaserlist/teaser-list/teaser-list.component";
    
    const routes: Routes = [
      { path: '', component: TeaserListComponent }
    ];
    
    @NgModule({
      imports: [RouterModule.forRoot(routes, {
        initialNavigation: 'enabledBlocking'
    })],
      exports: [RouterModule]
    })
    export class AppRoutingModule { }

TeaserListComponent is the one that uses HttpClient to get the JSON in its ngOnInit.

@front-a-little, please provide a minimal reproduction.

You can read here why this is needed. A good way to make a minimal repro is to create a new app via ng new repro-app and adding the minimum possible code to show the problem. Then you can push this repository to github and link it here.

Took me a while to verify this was not just happening to me, however, I can't reproduce it on a newly set up project, only on this project. At least it works for my colleagues so it isn'T just my installation of the project but something in the project itself.
Here's the sample repo, please keep in mind I fiddled around with a lot of stuff so the structure might not be optimal

The is the same root cause of angular/angular#49425 which was addressed in version 16.

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.