Correct semantics for XMLHttpRequest.responseText

Summary:
Accessing the `responseText` property when `responseType` is not `''` or `'text'` should throw. Also, the property is read-only.

**Test Plan:** UIExplorer example, unit tests
Closes https://github.com/facebook/react-native/pull/7284

Differential Revision: D3366893

fbshipit-source-id: a4cf5ebabcd1e03d6e2dc9d51230982922746c11
This commit is contained in:
Philipp von Weitershausen 2016-06-02 18:07:36 -07:00 committed by Facebook Github Bot 4
parent 16a97c8027
commit e29350214a
2 changed files with 83 additions and 20 deletions

View File

@ -85,7 +85,6 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
readyState: number = UNSENT; readyState: number = UNSENT;
responseHeaders: ?Object; responseHeaders: ?Object;
responseText: string = '';
status: number = 0; status: number = 0;
timeout: number = 0; timeout: number = 0;
responseURL: ?string; responseURL: ?string;
@ -103,6 +102,7 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
_method: ?string = null; _method: ?string = null;
_response: string | ?Object; _response: string | ?Object;
_responseType: ResponseType; _responseType: ResponseType;
_responseText: string = '';
_sent: boolean; _sent: boolean;
_url: ?string = null; _url: ?string = null;
_timedOut: boolean = false; _timedOut: boolean = false;
@ -116,7 +116,6 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
_reset(): void { _reset(): void {
this.readyState = this.UNSENT; this.readyState = this.UNSENT;
this.responseHeaders = undefined; this.responseHeaders = undefined;
this.responseText = '';
this.status = 0; this.status = 0;
delete this.responseURL; delete this.responseURL;
@ -125,6 +124,7 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
this._cachedResponse = undefined; this._cachedResponse = undefined;
this._hasError = false; this._hasError = false;
this._headers = {}; this._headers = {};
this._responseText = '';
this._responseType = ''; this._responseType = '';
this._sent = false; this._sent = false;
this._lowerCaseResponseHeaders = {}; this._lowerCaseResponseHeaders = {};
@ -148,7 +148,9 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
} }
if (!SUPPORTED_RESPONSE_TYPES.hasOwnProperty(responseType)) { if (!SUPPORTED_RESPONSE_TYPES.hasOwnProperty(responseType)) {
warning( warning(
`The provided value '${responseType}' is not a valid 'responseType'.`); false,
`The provided value '${responseType}' is not a valid 'responseType'.`
);
return; return;
} }
@ -160,13 +162,27 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
this._responseType = responseType; this._responseType = responseType;
} }
// $FlowIssue #10784535
get responseText(): string {
if (this._responseType !== '' && this._responseType !== 'text') {
throw new Error(
`The 'responseText' property is only available if 'responseType' ` +
`is set to '' or 'text', but it is '${this._responseType}'.`
);
}
if (this.readyState < LOADING) {
return '';
}
return this._responseText;
}
// $FlowIssue #10784535 // $FlowIssue #10784535
get response(): Response { get response(): Response {
const {responseType} = this; const {responseType} = this;
if (responseType === '' || responseType === 'text') { if (responseType === '' || responseType === 'text') {
return this.readyState < LOADING || this._hasError return this.readyState < LOADING || this._hasError
? '' ? ''
: this.responseText; : this._responseText;
} }
if (this.readyState !== DONE) { if (this.readyState !== DONE) {
@ -177,26 +193,26 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
return this._cachedResponse; return this._cachedResponse;
} }
switch (this.responseType) { switch (this._responseType) {
case 'document': case 'document':
this._cachedResponse = null; this._cachedResponse = null;
break; break;
case 'arraybuffer': case 'arraybuffer':
this._cachedResponse = toArrayBuffer( this._cachedResponse = toArrayBuffer(
this.responseText, this.getResponseHeader('content-type') || ''); this._responseText, this.getResponseHeader('content-type') || '');
break; break;
case 'blob': case 'blob':
this._cachedResponse = new global.Blob( this._cachedResponse = new global.Blob(
[this.responseText], [this._responseText],
{type: this.getResponseHeader('content-type') || ''} {type: this.getResponseHeader('content-type') || ''}
); );
break; break;
case 'json': case 'json':
try { try {
this._cachedResponse = JSON.parse(this.responseText); this._cachedResponse = JSON.parse(this._responseText);
} catch (_) { } catch (_) {
this._cachedResponse = null; this._cachedResponse = null;
} }
@ -226,7 +242,12 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
} }
} }
_didReceiveResponse(requestId: number, status: number, responseHeaders: ?Object, responseURL: ?string): void { __didReceiveResponse(
requestId: number,
status: number,
responseHeaders: ?Object,
responseURL: ?string
): void {
if (requestId === this._requestId) { if (requestId === this._requestId) {
this.status = status; this.status = status;
this.setResponseHeaders(responseHeaders); this.setResponseHeaders(responseHeaders);
@ -239,12 +260,12 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
} }
} }
_didReceiveData(requestId: number, responseText: string): void { __didReceiveData(requestId: number, responseText: string): void {
if (requestId === this._requestId) { if (requestId === this._requestId) {
if (!this.responseText) { if (!this._responseText) {
this.responseText = responseText; this._responseText = responseText;
} else { } else {
this.responseText += responseText; this._responseText += responseText;
} }
this._cachedResponse = undefined; // force lazy recomputation this._cachedResponse = undefined; // force lazy recomputation
this.setReadyState(this.LOADING); this.setReadyState(this.LOADING);
@ -252,10 +273,14 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
} }
// exposed for testing // exposed for testing
__didCompleteResponse(requestId: number, error: string, timeOutError: boolean): void { __didCompleteResponse(
requestId: number,
error: string,
timeOutError: boolean
): void {
if (requestId === this._requestId) { if (requestId === this._requestId) {
if (error) { if (error) {
this.responseText = error; this._responseText = error;
this._hasError = true; this._hasError = true;
if (timeOutError) { if (timeOutError) {
this._timedOut = true; this._timedOut = true;
@ -330,11 +355,11 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
)); ));
this._subscriptions.push(RCTNetworking.addListener( this._subscriptions.push(RCTNetworking.addListener(
'didReceiveNetworkResponse', 'didReceiveNetworkResponse',
(args) => this._didReceiveResponse(...args) (args) => this.__didReceiveResponse(...args)
)); ));
this._subscriptions.push(RCTNetworking.addListener( this._subscriptions.push(RCTNetworking.addListener(
'didReceiveNetworkData', 'didReceiveNetworkData',
(args) => this._didReceiveData(...args) (args) => this.__didReceiveData(...args)
)); ));
this._subscriptions.push(RCTNetworking.addListener( this._subscriptions.push(RCTNetworking.addListener(
'didCompleteNetworkResponse', 'didCompleteNetworkResponse',

View File

@ -56,15 +56,53 @@ describe('XMLHttpRequest', function(){
handleLoad = null; handleLoad = null;
}); });
it('should transition readyState correctly', function() { it('should transition readyState correctly', function() {
expect(xhr.readyState).toBe(xhr.UNSENT);
expect(xhr.readyState).toBe(xhr.UNSENT);
xhr.open('GET', 'blabla'); xhr.open('GET', 'blabla');
expect(xhr.onreadystatechange.mock.calls.length).toBe(1); expect(xhr.onreadystatechange.mock.calls.length).toBe(1);
expect(handleReadyStateChange.mock.calls.length).toBe(1); expect(handleReadyStateChange.mock.calls.length).toBe(1);
expect(xhr.readyState).toBe(xhr.OPENED); expect(xhr.readyState).toBe(xhr.OPENED);
}); });
it('should expose responseType correctly', function() {
expect(xhr.responseType).toBe('');
// Setting responseType to an unsupported value has no effect.
xhr.responseType = 'arrayblobbuffertextfile';
expect(xhr.responseType).toBe('');
xhr.responseType = 'arraybuffer';
expect(xhr.responseType).toBe('arraybuffer');
// Can't change responseType after first data has been received.
xhr.__didReceiveData(1, 'Some data');
expect(() => { xhr.responseType = 'text'; }).toThrow();
});
it('should expose responseText correctly', function() {
xhr.responseType = '';
expect(xhr.responseText).toBe('');
expect(xhr.response).toBe('');
xhr.responseType = 'arraybuffer';
expect(() => xhr.responseText).toThrow();
expect(xhr.response).toBe(null);
xhr.responseType = 'text';
expect(xhr.responseText).toBe('');
expect(xhr.response).toBe('');
// responseText is read-only.
expect(() => { xhr.responseText = 'hi'; }).toThrow();
expect(xhr.responseText).toBe('');
expect(xhr.response).toBe('');
xhr.__didReceiveData(1, 'Some data');
expect(xhr.responseText).toBe('Some data');
});
it('should call ontimeout function when the request times out', function(){ it('should call ontimeout function when the request times out', function(){
xhr.__didCompleteResponse(1, 'Timeout', true); xhr.__didCompleteResponse(1, 'Timeout', true);