Skip to content

Commit 45a6ac0

Browse files
alan-agius4atscott
authored andcommitted
fix(http): force macro task creation during HTTP request (#49546)
This commit adds a background macrotask when an XHR request is performed. The macrotask is started during `loadstart` and ended during `loadend` event. The macrotask is needed so that the application is not stabilized during HTTP calls. This is important for server rendering, as the application is rendering when the application is stabilized. The application is stabilized when there are no longer pending Macro and Micro tasks intercepted by Zone.js, Since an XHR request is none of these, we create a background macrotask so that Zone.js is made aware that there is something pending. Prior to this change, we patched the `HttpHandler` in `@angular/platform-server` but this is not enough, as there can be multiple `HttpHandler` in an application, example when importing `HttpClient` in a lazy loaded component/module. Which causes a new unpatched instance of `HttpHandler` to be created in the child injector which is not intercepted by Zone.js and thus the application is stabalized and rendered before the XHR request is finalized. NB: Zone.js is fundamental for SSR and currently, it's not possible to do SSR without it. Closes: #49425 PR Close #49546
1 parent ef149de commit 45a6ac0

File tree

15 files changed

+261
-126
lines changed

15 files changed

+261
-126
lines changed

goldens/public-api/common/http/index.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as i0 from '@angular/core';
1010
import { InjectionToken } from '@angular/core';
1111
import { ModuleWithProviders } from '@angular/core';
1212
import { Observable } from 'rxjs';
13+
import { OnDestroy } from '@angular/core';
1314
import { Provider } from '@angular/core';
1415
import { XhrFactory } from '@angular/common';
1516

@@ -2128,10 +2129,12 @@ export interface HttpUserEvent<T> {
21282129
}
21292130

21302131
// @public
2131-
export class HttpXhrBackend implements HttpBackend {
2132+
export class HttpXhrBackend implements HttpBackend, OnDestroy {
21322133
constructor(xhrFactory: XhrFactory);
21332134
handle(req: HttpRequest<any>): Observable<HttpEvent<any>>;
21342135
// (undocumented)
2136+
ngOnDestroy(): void;
2137+
// (undocumented)
21352138
static ɵfac: i0.ɵɵFactoryDeclaration<HttpXhrBackend, never>;
21362139
// (undocumented)
21372140
static ɵprov: i0.ɵɵInjectableDeclaration<HttpXhrBackend>;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {browser, by, element} from 'protractor';
10+
11+
import {verifyNoBrowserErrors} from './util';
12+
13+
describe('Http TransferState Lazy', function() {
14+
it('should transfer http state in lazy component', function() {
15+
// Load the page without waiting for Angular since it is not bootstrapped automatically.
16+
browser.driver.get(browser.baseUrl + 'http-transferstate-lazy');
17+
18+
// Test the contents from the server.
19+
const serverDiv = browser.driver.findElement(by.css('div'));
20+
expect(serverDiv.getText()).toBe('API response');
21+
22+
// Bootstrap the client side app and retest the contents
23+
browser.executeScript('doBootstrap()');
24+
expect(element(by.css('div')).getText()).toBe('API response');
25+
26+
// Make sure there were no client side errors.
27+
verifyNoBrowserErrors();
28+
});
29+
});

integration/platform-server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@angular/platform-browser": "file:../../dist/packages-dist/platform-browser",
1717
"@angular/platform-browser-dynamic": "file:../../dist/packages-dist/platform-browser-dynamic",
1818
"@angular/platform-server": "file:../../dist/packages-dist/platform-server",
19+
"@angular/router": "file:../../dist/packages-dist/router",
1920
"express": "4.16.4",
2021
"rxjs": "file:../../node_modules/rxjs",
2122
"typescript": "file:../../node_modules/typescript",
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Component } from '@angular/core';
2+
3+
@Component({
4+
selector: 'app-root',
5+
template: '<router-outlet></router-outlet>',
6+
})
7+
export class AppComponent {
8+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {NgModule} from '@angular/core';
10+
import {ServerModule} from '@angular/platform-server';
11+
12+
import {HttpLazyTransferStateModule} from './app';
13+
import {AppComponent} from './app.component';
14+
15+
@NgModule({
16+
bootstrap: [AppComponent],
17+
imports: [HttpLazyTransferStateModule, ServerModule],
18+
})
19+
export class HttpLazyTransferStateServerModule {
20+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {NgModule} from '@angular/core';
10+
import {BrowserModule} from '@angular/platform-browser';
11+
import {RouterModule, Routes} from '@angular/router';
12+
import {AppComponent} from './app.component';
13+
14+
const routes: Routes = [
15+
{
16+
path: '',
17+
loadChildren: () => import('./transfer-state.module').then((m) => m.TransferStateModule),
18+
},
19+
];
20+
21+
@NgModule({
22+
declarations: [AppComponent],
23+
bootstrap: [AppComponent],
24+
imports: [
25+
BrowserModule,
26+
RouterModule.forRoot(routes),
27+
],
28+
})
29+
export class HttpLazyTransferStateModule {
30+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import 'zone.js/bundles/zone.umd';
10+
11+
import {platformBrowser} from '@angular/platform-browser';
12+
import {HttpLazyTransferStateModule} from './app';
13+
14+
window['doBootstrap'] = function() {
15+
platformBrowser().bootstrapModule(HttpLazyTransferStateModule);
16+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<html>
2+
<head>
3+
<base href="/http-transferstate-lazy">
4+
<meta charset="UTF-8">
5+
<title>Hello World</title>
6+
<script src="webpack-out/httptransferstatelazy-bundle.js"></script>
7+
</head>
8+
<body>
9+
<app-root></app-root>
10+
</body>
11+
</html>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {isPlatformServer} from '@angular/common';
10+
import {HttpClient} from '@angular/common/http';
11+
import {Component, Inject, PLATFORM_ID} from '@angular/core';
12+
import {TransferState, makeStateKey} from '@angular/platform-browser';
13+
14+
const httpCacheKey = makeStateKey<string>('http');
15+
16+
@Component({
17+
selector: 'transfer-state-app-http',
18+
template: `
19+
<div>{{ response }}</div>
20+
`,
21+
})
22+
export class TransferStateComponent {
23+
response: string = '';
24+
25+
constructor(
26+
@Inject(PLATFORM_ID) private platformId: {},
27+
private readonly httpClient: HttpClient,
28+
private readonly transferState: TransferState
29+
) {}
30+
31+
ngOnInit() {
32+
if (isPlatformServer(this.platformId)) {
33+
this.httpClient.get<any>(`http://localhost:4206/api`).subscribe((response) => {
34+
this.transferState.set(httpCacheKey, response.data);
35+
this.response = response.data;
36+
});
37+
} else {
38+
this.response = this.transferState.get(httpCacheKey, '');
39+
}
40+
}
41+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { CommonModule } from '@angular/common';
2+
import { HttpClientModule } from '@angular/common/http';
3+
import { NgModule } from '@angular/core';
4+
import { RouterModule, Routes } from '@angular/router';
5+
import { TransferStateComponent } from './transfer-state.component';
6+
7+
const routes: Routes = [
8+
{
9+
path: '',
10+
component: TransferStateComponent,
11+
},
12+
];
13+
14+
@NgModule({
15+
imports: [RouterModule.forChild(routes), HttpClientModule, CommonModule],
16+
declarations: [TransferStateComponent],
17+
exports: [TransferStateComponent],
18+
})
19+
export class TransferStateModule {
20+
}

0 commit comments

Comments
 (0)