Skip to content

Commit 8821723

Browse files
alxhubjasonaden
authored andcommitted
fix(common): fix XSSI prefix stripping by using JSON.parse always (#18466)
Currently HttpClient sends requests for JSON data with the XMLHttpRequest.responseType set to 'json'. With this flag, the browser will attempt to parse the response as JSON, but will return 'null' on any errors. If the JSON response contains an XSSI-prevention prefix, this will cause the browser's parsing to fail, which is unrecoverable. The only compelling reason to use the responseType 'json' is for performance (especially if the browser offloads JSON parsing to a separate thread). I'm not aware of any browser which does this currently, nor of any plans to do so. JSON.parse and responseType 'json' both end up using the same V8 code path in Chrome to implement the parse. Thus, this change switches all JSON parsing in HttpClient to use JSON.parse directly. Fixes #18396, #18453. PR Close #18466
1 parent a203a95 commit 8821723

File tree

3 files changed

+25
-15
lines changed

3 files changed

+25
-15
lines changed

packages/common/http/src/xhr.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,14 @@ export class HttpXhrBackend implements HttpBackend {
107107

108108
// Set the responseType if one was requested.
109109
if (req.responseType) {
110-
xhr.responseType = req.responseType.toLowerCase() as any;
110+
const responseType = req.responseType.toLowerCase();
111+
112+
// JSON responses need to be processed as text. This is because if the server
113+
// returns an XSSI-prefixed JSON response, the browser will fail to parse it,
114+
// xhr.response will be null, and xhr.responseText cannot be accessed to
115+
// retrieve the prefixed JSON data in order to strip the prefix. Thus, all JSON
116+
// is parsed by first requesting text and then applying JSON.parse.
117+
xhr.responseType = ((responseType !== 'json') ? responseType : 'text') as any;
111118
}
112119

113120
// Serialize the request body if one is present. If not, this will be set to null.
@@ -158,12 +165,6 @@ export class HttpXhrBackend implements HttpBackend {
158165
if (status !== 204) {
159166
// Use XMLHttpRequest.response if set, responseText otherwise.
160167
body = (typeof xhr.response === 'undefined') ? xhr.responseText : xhr.response;
161-
162-
// Strip a common XSSI prefix from string responses.
163-
// TODO: determine if this behavior should be optional and moved to an interceptor.
164-
if (typeof body === 'string') {
165-
body = body.replace(XSSI_PREFIX, '');
166-
}
167168
}
168169

169170
// Normalize another potential bug (this one comes from CORS).
@@ -179,8 +180,9 @@ export class HttpXhrBackend implements HttpBackend {
179180

180181
// Check whether the body needs to be parsed as JSON (in many cases the browser
181182
// will have done that already).
182-
if (ok && typeof body === 'string' && req.responseType === 'json') {
183+
if (ok && req.responseType === 'json' && typeof body === 'string') {
183184
// Attempt the parse. If it fails, a parse error should be delivered to the user.
185+
body = body.replace(XSSI_PREFIX, '');
184186
try {
185187
body = JSON.parse(body);
186188
} catch (error) {

packages/common/http/test/xhr_mock.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ export class MockXMLHttpRequest {
7979
return new HttpHeaders(this.mockResponseHeaders).get(header);
8080
}
8181

82-
mockFlush(status: number, statusText: string, body: any|null) {
83-
if (this.responseType === 'text') {
82+
mockFlush(status: number, statusText: string, body?: string) {
83+
if (typeof body === 'string') {
8484
this.responseText = body;
8585
} else {
8686
this.response = body;

packages/common/http/test/xhr_spec.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {ddescribe, describe, it} from '@angular/core/testing/src/testing_internal';
9+
import {ddescribe, describe, iit, it} from '@angular/core/testing/src/testing_internal';
1010
import {Observable} from 'rxjs/Observable';
1111

1212
import {HttpRequest} from '../src/request';
@@ -87,14 +87,22 @@ export function main() {
8787
});
8888
it('handles a json response', () => {
8989
const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
90-
factory.mock.mockFlush(200, 'OK', {data: 'some data'});
90+
factory.mock.mockFlush(200, 'OK', JSON.stringify({data: 'some data'}));
9191
expect(events.length).toBe(2);
9292
const res = events[1] as HttpResponse<{data: string}>;
9393
expect(res.body !.data).toBe('some data');
9494
});
95-
it('handles a json response that comes via responseText', () => {
95+
it('handles a json string response', () => {
9696
const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
97-
factory.mock.mockFlush(200, 'OK', JSON.stringify({data: 'some data'}));
97+
expect(factory.mock.responseType).toEqual('text');
98+
factory.mock.mockFlush(200, 'OK', JSON.stringify('this is a string'));
99+
expect(events.length).toBe(2);
100+
const res = events[1] as HttpResponse<string>;
101+
expect(res.body).toEqual('this is a string');
102+
});
103+
it('handles a json response with an XSSI prefix', () => {
104+
const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
105+
factory.mock.mockFlush(200, 'OK', ')]}\'\n' + JSON.stringify({data: 'some data'}));
98106
expect(events.length).toBe(2);
99107
const res = events[1] as HttpResponse<{data: string}>;
100108
expect(res.body !.data).toBe('some data');
@@ -299,7 +307,7 @@ export function main() {
299307
expect(error.status).toBe(0);
300308
done();
301309
});
302-
factory.mock.mockFlush(0, 'CORS 0 status', null);
310+
factory.mock.mockFlush(0, 'CORS 0 status');
303311
});
304312
});
305313
});

0 commit comments

Comments
 (0)