Skip to content

Commit 9559d3e

Browse files
alxhubIgorMinar
authored andcommitted
feat(platform-server): support @angular/http from @angular/platform-server
This change installs HttpModule with ServerModule, and overrides bindings to service Http requests made from the server with the 'xhr2' NPM package. Outgoing requests are wrapped in a Zone macro-task, so they will be tracked within the Angular zone and cause the isStable API to show 'false' until they return. This is essential for Universal support of server-side HTTP.
1 parent 30380d0 commit 9559d3e

File tree

13 files changed

+270
-9
lines changed

13 files changed

+270
-9
lines changed

build.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ PACKAGES=(core
1010
forms
1111
platform-browser
1212
platform-browser-dynamic
13+
http
1314
platform-server
1415
platform-webworker
1516
platform-webworker-dynamic
1617
animation
17-
http
1818
upgrade
1919
router
2020
compiler-cli

modules/@angular/compiler-cli/tsconfig-build.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"@angular/core": ["../../../dist/packages-dist/core"],
1111
"@angular/common": ["../../../dist/packages-dist/common"],
1212
"@angular/compiler": ["../../../dist/packages-dist/compiler"],
13+
"@angular/http": ["../../../dist/packages-dist/http"],
1314
"@angular/platform-server": ["../../../dist/packages-dist/platform-server"],
1415
"@angular/platform-browser": ["../../../dist/packages-dist/platform-browser"],
1516
"@angular/tsc-wrapped": ["../../../dist/tools/@angular/tsc-wrapped"]

modules/@angular/language-service/tsconfig-build.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@angular/compiler": ["../../../dist/packages-dist/compiler"],
1818
"@angular/compiler/*": ["../../../dist/packages-dist/compiler/*"],
1919
"@angular/compiler-cli": ["../../../dist/packages-dist/compiler-cli"],
20+
"@angular/http": ["../../../dist/packages-dist/http"],
2021
"@angular/platform-server": ["../../../dist/packages-dist/platform-server"],
2122
"@angular/platform-browser": ["../../../dist/packages-dist/platform-browser"],
2223
"@angular/tsc-wrapped": ["../../../dist/tools/@angular/tsc-wrapped"],

modules/@angular/platform-server/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"@angular/platform-browser": "0.0.0-PLACEHOLDER"
1515
},
1616
"dependencies": {
17-
"parse5": "^2.2.1"
17+
"parse5": "^2.2.1",
18+
"xhr2": "^0.1.4"
1819
},
1920
"repository": {
2021
"type": "git",
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. 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+
const xhr2: any = require('xhr2');
10+
11+
import {Injectable, Provider} from '@angular/core';
12+
import {BrowserXhr, Connection, ConnectionBackend, Http, ReadyState, Request, RequestOptions, Response, XHRBackend, XSRFStrategy} from '@angular/http';
13+
14+
import {Observable} from 'rxjs/Observable';
15+
import {Observer} from 'rxjs/Observer';
16+
import {Subscription} from 'rxjs/Subscription';
17+
18+
@Injectable()
19+
export class ServerXhr implements BrowserXhr {
20+
build(): XMLHttpRequest { return new xhr2.XMLHttpRequest(); }
21+
}
22+
23+
@Injectable()
24+
export class ServerXsrfStrategy implements XSRFStrategy {
25+
configureRequest(req: Request): void {}
26+
}
27+
28+
export class ZoneMacroTaskConnection implements Connection {
29+
response: Observable<Response>;
30+
lastConnection: Connection;
31+
32+
constructor(public request: Request, backend: XHRBackend) {
33+
this.response = new Observable((observer: Observer<Response>) => {
34+
let task: Task = null;
35+
let scheduled: boolean = false;
36+
let sub: Subscription = null;
37+
let savedResult: any = null;
38+
let savedError: any = null;
39+
40+
const scheduleTask = (_task: Task) => {
41+
task = _task;
42+
scheduled = true;
43+
44+
this.lastConnection = backend.createConnection(request);
45+
sub = (this.lastConnection.response as Observable<Response>)
46+
.subscribe(
47+
res => savedResult = res,
48+
err => {
49+
if (!scheduled) {
50+
throw new Error('invoke twice');
51+
}
52+
savedError = err;
53+
scheduled = false;
54+
task.invoke();
55+
},
56+
() => {
57+
if (!scheduled) {
58+
throw new Error('invoke twice');
59+
}
60+
scheduled = false;
61+
task.invoke();
62+
});
63+
};
64+
65+
const cancelTask = (_task: Task) => {
66+
if (!scheduled) {
67+
return;
68+
}
69+
scheduled = false;
70+
if (sub) {
71+
sub.unsubscribe();
72+
sub = null;
73+
}
74+
};
75+
76+
const onComplete = () => {
77+
if (savedError !== null) {
78+
observer.error(savedError);
79+
} else {
80+
observer.next(savedResult);
81+
observer.complete();
82+
}
83+
};
84+
85+
// MockBackend is currently synchronous, which means that if scheduleTask is by
86+
// scheduleMacroTask, the request will hit MockBackend and the response will be
87+
// sent, causing task.invoke() to be called.
88+
const _task = Zone.current.scheduleMacroTask(
89+
'ZoneMacroTaskConnection.subscribe', onComplete, {}, () => null, cancelTask);
90+
scheduleTask(_task);
91+
92+
return () => {
93+
if (scheduled && task) {
94+
task.zone.cancelTask(task);
95+
scheduled = false;
96+
}
97+
if (sub) {
98+
sub.unsubscribe();
99+
sub = null;
100+
}
101+
};
102+
});
103+
}
104+
105+
get readyState(): ReadyState {
106+
return !!this.lastConnection ? this.lastConnection.readyState : ReadyState.Unsent;
107+
}
108+
}
109+
110+
export class ZoneMacroTaskBackend implements ConnectionBackend {
111+
constructor(private backend: XHRBackend) {}
112+
113+
createConnection(request: any): ZoneMacroTaskConnection {
114+
return new ZoneMacroTaskConnection(request, this.backend);
115+
}
116+
}
117+
118+
export function httpFactory(xhrBackend: XHRBackend, options: RequestOptions) {
119+
const macroBackend = new ZoneMacroTaskBackend(xhrBackend);
120+
return new Http(macroBackend, options);
121+
}
122+
123+
export const SERVER_HTTP_PROVIDERS: Provider[] = [
124+
{provide: Http, useFactory: httpFactory, deps: [XHRBackend, RequestOptions]},
125+
{provide: BrowserXhr, useClass: ServerXhr},
126+
{provide: XSRFStrategy, useClass: ServerXsrfStrategy},
127+
];

modules/@angular/platform-server/src/server.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88

99
import {PlatformLocation} from '@angular/common';
1010
import {platformCoreDynamic} from '@angular/compiler';
11-
import {APP_BOOTSTRAP_LISTENER, InjectionToken, Injector, NgModule, PLATFORM_INITIALIZER, PlatformRef, Provider, RENDERER_V2_DIRECT, RendererV2, RootRenderer, createPlatformFactory, isDevMode, platformCore} from '@angular/core';
11+
import {APP_BOOTSTRAP_LISTENER, Injectable, InjectionToken, Injector, NgModule, PLATFORM_INITIALIZER, PlatformRef, Provider, RENDERER_V2_DIRECT, RendererV2, RootRenderer, createPlatformFactory, isDevMode, platformCore} from '@angular/core';
12+
import {HttpModule} from '@angular/http';
1213
import {BrowserModule, DOCUMENT} from '@angular/platform-browser';
14+
15+
import {SERVER_HTTP_PROVIDERS} from './http';
1316
import {ServerPlatformLocation} from './location';
1417
import {Parse5DomAdapter, parseDocument} from './parse5_adapter';
1518
import {PlatformState} from './platform_state';
@@ -86,9 +89,8 @@ export const INITIAL_CONFIG = new InjectionToken<PlatformConfig>('Server.INITIAL
8689
*/
8790
@NgModule({
8891
exports: [BrowserModule],
89-
providers: [
90-
SERVER_RENDER_PROVIDERS,
91-
]
92+
imports: [HttpModule],
93+
providers: [SERVER_RENDER_PROVIDERS, SERVER_HTTP_PROVIDERS],
9294
})
9395
export class ServerModule {
9496
}

modules/@angular/platform-server/test/integration_spec.ts

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
*/
88

99
import {PlatformLocation} from '@angular/common';
10-
import {ApplicationRef, CompilerFactory, Component, NgModule, NgModuleRef, PlatformRef, destroyPlatform, getPlatform} from '@angular/core';
10+
import {ApplicationRef, CompilerFactory, Component, NgModule, NgModuleRef, NgZone, PlatformRef, destroyPlatform, getPlatform} from '@angular/core';
1111
import {async, inject} from '@angular/core/testing';
12+
import {Http, HttpModule, Response, ResponseOptions, XHRBackend} from '@angular/http';
13+
import {MockBackend, MockConnection} from '@angular/http/testing';
1214
import {DOCUMENT} from '@angular/platform-browser';
1315
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
1416
import {INITIAL_CONFIG, PlatformState, ServerModule, platformDynamicServer, renderModule, renderModuleFactory} from '@angular/platform-server';
17+
1518
import {Subscription} from 'rxjs/Subscription';
1619
import {filter} from 'rxjs/operator/filter';
1720
import {first} from 'rxjs/operator/first';
@@ -21,7 +24,15 @@ import {toPromise} from 'rxjs/operator/toPromise';
2124
class MyServerApp {
2225
}
2326

24-
@NgModule({declarations: [MyServerApp], imports: [ServerModule], bootstrap: [MyServerApp]})
27+
@NgModule({
28+
bootstrap: [MyServerApp],
29+
declarations: [MyServerApp],
30+
imports: [ServerModule],
31+
providers: [
32+
MockBackend,
33+
{provide: XHRBackend, useExisting: MockBackend},
34+
]
35+
})
2536
class ExampleModule {
2637
}
2738

@@ -55,6 +66,30 @@ class MyStylesApp {
5566
class ExampleStylesModule {
5667
}
5768

69+
@NgModule({
70+
bootstrap: [MyServerApp],
71+
declarations: [MyServerApp],
72+
imports: [HttpModule, ServerModule],
73+
providers: [
74+
MockBackend,
75+
{provide: XHRBackend, useExisting: MockBackend},
76+
]
77+
})
78+
export class HttpBeforeExampleModule {
79+
}
80+
81+
@NgModule({
82+
bootstrap: [MyServerApp],
83+
declarations: [MyServerApp],
84+
imports: [ServerModule, HttpModule],
85+
providers: [
86+
MockBackend,
87+
{provide: XHRBackend, useExisting: MockBackend},
88+
]
89+
})
90+
export class HttpAfterExampleModule {
91+
}
92+
5893
export function main() {
5994
if (getDOM().supportsDOMEvents()) return; // NODE only
6095

@@ -196,5 +231,86 @@ export function main() {
196231
});
197232
})));
198233
});
234+
235+
describe('http', () => {
236+
it('can inject Http', async(() => {
237+
const platform = platformDynamicServer(
238+
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
239+
platform.bootstrapModule(ExampleModule).then(ref => {
240+
expect(ref.injector.get(Http) instanceof Http).toBeTruthy();
241+
});
242+
}));
243+
it('can make Http requests', async(() => {
244+
const platform = platformDynamicServer(
245+
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
246+
platform.bootstrapModule(ExampleModule).then(ref => {
247+
const mock = ref.injector.get(MockBackend);
248+
const http = ref.injector.get(Http);
249+
ref.injector.get(NgZone).run(() => {
250+
NgZone.assertInAngularZone();
251+
mock.connections.subscribe((mc: MockConnection) => {
252+
NgZone.assertInAngularZone();
253+
expect(mc.request.url).toBe('/testing');
254+
mc.mockRespond(new Response(new ResponseOptions({body: 'success!', status: 200})));
255+
});
256+
http.get('/testing').subscribe(resp => {
257+
NgZone.assertInAngularZone();
258+
expect(resp.text()).toBe('success!');
259+
});
260+
});
261+
});
262+
}));
263+
it('requests are macrotasks', async(() => {
264+
const platform = platformDynamicServer(
265+
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
266+
platform.bootstrapModule(ExampleModule).then(ref => {
267+
const mock = ref.injector.get(MockBackend);
268+
const http = ref.injector.get(Http);
269+
expect(ref.injector.get(NgZone).hasPendingMacrotasks).toBeFalsy();
270+
ref.injector.get(NgZone).run(() => {
271+
NgZone.assertInAngularZone();
272+
mock.connections.subscribe((mc: MockConnection) => {
273+
expect(ref.injector.get(NgZone).hasPendingMacrotasks).toBeTruthy();
274+
mc.mockRespond(new Response(new ResponseOptions({body: 'success!', status: 200})));
275+
});
276+
http.get('/testing').subscribe(resp => { expect(resp.text()).toBe('success!'); });
277+
});
278+
});
279+
}));
280+
it('works when HttpModule is included before ServerModule', async(() => {
281+
const platform = platformDynamicServer(
282+
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
283+
platform.bootstrapModule(HttpBeforeExampleModule).then(ref => {
284+
const mock = ref.injector.get(MockBackend);
285+
const http = ref.injector.get(Http);
286+
expect(ref.injector.get(NgZone).hasPendingMacrotasks).toBeFalsy();
287+
ref.injector.get(NgZone).run(() => {
288+
NgZone.assertInAngularZone();
289+
mock.connections.subscribe((mc: MockConnection) => {
290+
expect(ref.injector.get(NgZone).hasPendingMacrotasks).toBeTruthy();
291+
mc.mockRespond(new Response(new ResponseOptions({body: 'success!', status: 200})));
292+
});
293+
http.get('/testing').subscribe(resp => { expect(resp.text()).toBe('success!'); });
294+
});
295+
});
296+
}));
297+
it('works when HttpModule is included after ServerModule', async(() => {
298+
const platform = platformDynamicServer(
299+
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
300+
platform.bootstrapModule(HttpAfterExampleModule).then(ref => {
301+
const mock = ref.injector.get(MockBackend);
302+
const http = ref.injector.get(Http);
303+
expect(ref.injector.get(NgZone).hasPendingMacrotasks).toBeFalsy();
304+
ref.injector.get(NgZone).run(() => {
305+
NgZone.assertInAngularZone();
306+
mock.connections.subscribe((mc: MockConnection) => {
307+
expect(ref.injector.get(NgZone).hasPendingMacrotasks).toBeTruthy();
308+
mc.mockRespond(new Response(new ResponseOptions({body: 'success!', status: 200})));
309+
});
310+
http.get('/testing').subscribe(resp => { expect(resp.text()).toBe('success!'); });
311+
});
312+
});
313+
}));
314+
});
199315
});
200316
}

modules/@angular/platform-server/tsconfig-build.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"@angular/core": ["../../../dist/packages-dist/core"],
1212
"@angular/common": ["../../../dist/packages-dist/common"],
1313
"@angular/compiler": ["../../../dist/packages-dist/compiler"],
14+
"@angular/http": ["../../../dist/packages-dist/http"],
1415
"@angular/platform-browser": ["../../../dist/packages-dist/platform-browser"],
1516
"@angular/platform-browser-dynamic": ["../../../dist/packages-dist/platform-browser-dynamic"]
1617
},

npm-shrinkwrap.clean.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6713,6 +6713,10 @@
67136713
"version": "2.0.0",
67146714
"dev": true
67156715
},
6716+
"xhr2": {
6717+
"version": "0.1.4",
6718+
"dev": true
6719+
},
67166720
"xml2js": {
67176721
"version": "0.4.15",
67186722
"dev": true

npm-shrinkwrap.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)