Add support for missing XHR response types
Summary:Fixes #6679 This adds support for the missing response types to XMLHttpRequest. Don?t ship this yet. This is completely untested. yolo and stuff. Closes https://github.com/facebook/react-native/pull/6870 Reviewed By: bestander Differential Revision: D3153628 Pulled By: davidaurelio fb-gh-sync-id: 76feae3377bc24b931548a9ac1af07943b1048ac fbshipit-source-id: 76feae3377bc24b931548a9ac1af07943b1048ac
This commit is contained in:
parent
c55ca61396
commit
fcc89e9d92
|
@ -13,7 +13,12 @@
|
||||||
|
|
||||||
var RCTNetworking = require('RCTNetworking');
|
var RCTNetworking = require('RCTNetworking');
|
||||||
var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');
|
var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');
|
||||||
var invariant = require('fbjs/lib/invariant');
|
const invariant = require('fbjs/lib/invariant');
|
||||||
|
const utf8 = require('utf8');
|
||||||
|
const warning = require('fbjs/lib/warning');
|
||||||
|
|
||||||
|
type ResponseType = '' | 'arraybuffer' | 'blob' | 'document' | 'json' | 'text';
|
||||||
|
type Response = ?Object | string;
|
||||||
|
|
||||||
const UNSENT = 0;
|
const UNSENT = 0;
|
||||||
const OPENED = 1;
|
const OPENED = 1;
|
||||||
|
@ -21,6 +26,15 @@ const HEADERS_RECEIVED = 2;
|
||||||
const LOADING = 3;
|
const LOADING = 3;
|
||||||
const DONE = 4;
|
const DONE = 4;
|
||||||
|
|
||||||
|
const SUPPORTED_RESPONSE_TYPES = {
|
||||||
|
arraybuffer: typeof global.ArrayBuffer === 'function',
|
||||||
|
blob: typeof global.Blob === 'function',
|
||||||
|
document: false,
|
||||||
|
json: true,
|
||||||
|
text: true,
|
||||||
|
'': true,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared base for platform-specific XMLHttpRequest implementations.
|
* Shared base for platform-specific XMLHttpRequest implementations.
|
||||||
*/
|
*/
|
||||||
|
@ -43,9 +57,7 @@ class XMLHttpRequestBase {
|
||||||
upload: any;
|
upload: any;
|
||||||
readyState: number;
|
readyState: number;
|
||||||
responseHeaders: ?Object;
|
responseHeaders: ?Object;
|
||||||
responseText: ?string;
|
responseText: string;
|
||||||
response: ?string;
|
|
||||||
responseType: '' | 'text';
|
|
||||||
status: number;
|
status: number;
|
||||||
timeout: number;
|
timeout: number;
|
||||||
responseURL: ?string;
|
responseURL: ?string;
|
||||||
|
@ -57,12 +69,16 @@ class XMLHttpRequestBase {
|
||||||
_requestId: ?number;
|
_requestId: ?number;
|
||||||
_subscriptions: [any];
|
_subscriptions: [any];
|
||||||
|
|
||||||
_method: ?string;
|
|
||||||
_url: ?string;
|
|
||||||
_headers: Object;
|
|
||||||
_sent: boolean;
|
|
||||||
_aborted: boolean;
|
_aborted: boolean;
|
||||||
|
_cachedResponse: Response;
|
||||||
|
_hasError: boolean;
|
||||||
|
_headers: Object;
|
||||||
_lowerCaseResponseHeaders: Object;
|
_lowerCaseResponseHeaders: Object;
|
||||||
|
_method: ?string;
|
||||||
|
_response: string | ?Object;
|
||||||
|
_responseType: ResponseType;
|
||||||
|
_sent: boolean;
|
||||||
|
_url: ?string;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.UNSENT = UNSENT;
|
this.UNSENT = UNSENT;
|
||||||
|
@ -82,24 +98,101 @@ class XMLHttpRequestBase {
|
||||||
this._aborted = false;
|
this._aborted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_reset() {
|
_reset(): void {
|
||||||
this.readyState = this.UNSENT;
|
this.readyState = this.UNSENT;
|
||||||
this.responseHeaders = undefined;
|
this.responseHeaders = undefined;
|
||||||
this.responseText = '';
|
this.responseText = '';
|
||||||
this.response = null;
|
|
||||||
this.responseType = '';
|
|
||||||
this.status = 0;
|
this.status = 0;
|
||||||
delete this.responseURL;
|
delete this.responseURL;
|
||||||
|
|
||||||
this._requestId = null;
|
this._requestId = null;
|
||||||
|
|
||||||
|
this._cachedResponse = undefined;
|
||||||
|
this._hasError = false;
|
||||||
this._headers = {};
|
this._headers = {};
|
||||||
|
this._responseType = '';
|
||||||
this._sent = false;
|
this._sent = false;
|
||||||
this._lowerCaseResponseHeaders = {};
|
this._lowerCaseResponseHeaders = {};
|
||||||
|
|
||||||
this._clearSubscriptions();
|
this._clearSubscriptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// $FlowIssue #10784535
|
||||||
|
get responseType(): ResponseType {
|
||||||
|
return this._responseType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $FlowIssue #10784535
|
||||||
|
set responseType(responseType: ResponseType): void {
|
||||||
|
if (this.readyState > HEADERS_RECEIVED) {
|
||||||
|
throw new Error(
|
||||||
|
"Failed to set the 'responseType' property on 'XMLHttpRequest': The " +
|
||||||
|
"response type cannot be set if the object's state is LOADING or DONE"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!SUPPORTED_RESPONSE_TYPES.hasOwnProperty(responseType)) {
|
||||||
|
warning(
|
||||||
|
`The provided value '${responseType}' is not a valid 'responseType'.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// redboxes early, e.g. for 'arraybuffer' on ios 7
|
||||||
|
invariant(
|
||||||
|
SUPPORTED_RESPONSE_TYPES[responseType] || responseType === 'document',
|
||||||
|
`The provided value '${responseType}' is unsupported in this environment.`
|
||||||
|
);
|
||||||
|
this._responseType = responseType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $FlowIssue #10784535
|
||||||
|
get response(): Response {
|
||||||
|
const {responseType} = this;
|
||||||
|
if (responseType === '' || responseType === 'text') {
|
||||||
|
return this.readyState < LOADING || this._hasError
|
||||||
|
? ''
|
||||||
|
: this.responseText;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.readyState !== DONE) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._cachedResponse !== undefined) {
|
||||||
|
return this._cachedResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (this.responseType) {
|
||||||
|
case 'document':
|
||||||
|
this._cachedResponse = null;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'arraybuffer':
|
||||||
|
this._cachedResponse = toArrayBuffer(
|
||||||
|
this.responseText, this.getResponseHeader('content-type') || '');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'blob':
|
||||||
|
this._cachedResponse = new global.Blob(
|
||||||
|
[this.responseText],
|
||||||
|
{type: this.getResponseHeader('content-type') || ''}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'json':
|
||||||
|
try {
|
||||||
|
this._cachedResponse = JSON.parse(this.responseText);
|
||||||
|
} catch (_) {
|
||||||
|
this._cachedResponse = null;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this._cachedResponse = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._cachedResponse;
|
||||||
|
}
|
||||||
|
|
||||||
didCreateRequest(requestId: number): void {
|
didCreateRequest(requestId: number): void {
|
||||||
this._requestId = requestId;
|
this._requestId = requestId;
|
||||||
this._subscriptions.push(RCTDeviceEventEmitter.addListener(
|
this._subscriptions.push(RCTDeviceEventEmitter.addListener(
|
||||||
|
@ -151,22 +244,7 @@ class XMLHttpRequestBase {
|
||||||
} else {
|
} else {
|
||||||
this.responseText += responseText;
|
this.responseText += responseText;
|
||||||
}
|
}
|
||||||
switch(this.responseType) {
|
this._cachedResponse = undefined; // force lazy recomputation
|
||||||
case '':
|
|
||||||
case 'text':
|
|
||||||
this.response = this.responseText;
|
|
||||||
break;
|
|
||||||
case 'blob': // whatwg-fetch sets this in Chrome
|
|
||||||
/* global Blob: true */
|
|
||||||
invariant(
|
|
||||||
typeof Blob === 'function',
|
|
||||||
`responseType "blob" is only supported on platforms with native Blob support`
|
|
||||||
);
|
|
||||||
this.response = new Blob([this.responseText]);
|
|
||||||
break;
|
|
||||||
default: //TODO: Support other types, eg: document, arraybuffer, json
|
|
||||||
invariant(false, `responseType "${this.responseType}" is unsupported`);
|
|
||||||
}
|
|
||||||
this.setReadyState(this.LOADING);
|
this.setReadyState(this.LOADING);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -175,6 +253,7 @@ class XMLHttpRequestBase {
|
||||||
if (requestId === this._requestId) {
|
if (requestId === this._requestId) {
|
||||||
if (error) {
|
if (error) {
|
||||||
this.responseText = error;
|
this.responseText = error;
|
||||||
|
this._hasError = true;
|
||||||
}
|
}
|
||||||
this._clearSubscriptions();
|
this._clearSubscriptions();
|
||||||
this._requestId = null;
|
this._requestId = null;
|
||||||
|
@ -304,4 +383,24 @@ XMLHttpRequestBase.HEADERS_RECEIVED = HEADERS_RECEIVED;
|
||||||
XMLHttpRequestBase.LOADING = LOADING;
|
XMLHttpRequestBase.LOADING = LOADING;
|
||||||
XMLHttpRequestBase.DONE = DONE;
|
XMLHttpRequestBase.DONE = DONE;
|
||||||
|
|
||||||
|
function toArrayBuffer(text: string, contentType: string): ArrayBuffer {
|
||||||
|
const {length} = text;
|
||||||
|
if (length === 0) {
|
||||||
|
return new ArrayBuffer(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const charsetMatch = contentType.match(/;\s*charset=([^;]*)/i);
|
||||||
|
const charset = charsetMatch ? charsetMatch[1].trim() : 'utf-8';
|
||||||
|
|
||||||
|
if (/^utf-?8$/i.test(charset)) {
|
||||||
|
return utf8.encode(text);
|
||||||
|
} else { //TODO: utf16 / ucs2 / utf32
|
||||||
|
const array = new Uint8Array(length);
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
array[i] = text.charCodeAt(i); // Uint8Array automatically masks with 0xff
|
||||||
|
}
|
||||||
|
return array.buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = XMLHttpRequestBase;
|
module.exports = XMLHttpRequestBase;
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2016-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
jest.autoMockOff();
|
||||||
|
|
||||||
|
const {encode} = require('../utf8');
|
||||||
|
|
||||||
|
describe('UTF-8 encoding:', () => {
|
||||||
|
it('can encode code points < U+80', () => {
|
||||||
|
const arrayBuffer = encode('\u0000abcDEF\u007f');
|
||||||
|
expect(new Uint8Array(arrayBuffer)).toEqual(
|
||||||
|
new Uint8Array([0x00, 0x61, 0x62, 0x63, 0x44, 0x45, 0x46, 0x7f]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can encode code points < U+800', () => {
|
||||||
|
const arrayBuffer = encode('\u0080\u0548\u07ff');
|
||||||
|
expect(new Uint8Array(arrayBuffer)).toEqual(
|
||||||
|
new Uint8Array([0xc2, 0x80, 0xd5, 0x88, 0xdf, 0xbf]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can encode code points < U+10000', () => {
|
||||||
|
const arrayBuffer = encode('\u0800\uac48\uffff');
|
||||||
|
expect(new Uint8Array(arrayBuffer)).toEqual(
|
||||||
|
new Uint8Array([0xe0, 0xa0, 0x80, 0xea, 0xb1, 0x88, 0xef, 0xbf, 0xbf]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can encode code points in the Supplementary Planes (surrogate pairs)', () => {
|
||||||
|
const arrayBuffer = encode([
|
||||||
|
'\ud800\udc00',
|
||||||
|
'\ud800\ude89',
|
||||||
|
'\ud83d\ude3b',
|
||||||
|
'\udbff\udfff'
|
||||||
|
].join(''));
|
||||||
|
expect(new Uint8Array(arrayBuffer)).toEqual(
|
||||||
|
new Uint8Array([
|
||||||
|
0xf0, 0x90, 0x80, 0x80,
|
||||||
|
0xf0, 0x90, 0x8a, 0x89,
|
||||||
|
0xf0, 0x9f, 0x98, 0xbb,
|
||||||
|
0xf4, 0x8f, 0xbf, 0xbf,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows for stray high surrogates', () => {
|
||||||
|
const arrayBuffer = encode('a\ud8c6b');
|
||||||
|
expect(new Uint8Array(arrayBuffer)).toEqual(
|
||||||
|
new Uint8Array([0x61, 0xed, 0xa3, 0x86, 0x62]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows for stray low surrogates', () => {
|
||||||
|
const arrayBuffer = encode('a\ude19b');
|
||||||
|
expect(new Uint8Array(arrayBuffer)).toEqual(
|
||||||
|
new Uint8Array([0x61, 0xed, 0xb8, 0x99, 0x62]));
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,91 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2016-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*
|
||||||
|
* @providesModule utf8
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
class ByteVector {
|
||||||
|
_storage: Uint8Array;
|
||||||
|
_sizeWritten: number;
|
||||||
|
|
||||||
|
constructor(size) {
|
||||||
|
this._storage = new Uint8Array(size);
|
||||||
|
this._sizeWritten = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
push(value: number): ByteVector {
|
||||||
|
const i = this._sizeWritten;
|
||||||
|
if (i === this._storage.length) {
|
||||||
|
this._realloc();
|
||||||
|
}
|
||||||
|
this._storage[i] = value;
|
||||||
|
this._sizeWritten = i + 1;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBuffer(): ArrayBuffer {
|
||||||
|
return this._storage.buffer.slice(0, this._sizeWritten);
|
||||||
|
}
|
||||||
|
|
||||||
|
_realloc() {
|
||||||
|
const storage = this._storage;
|
||||||
|
this._storage = new Uint8Array(align(storage.length * 1.5));
|
||||||
|
this._storage.set(storage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*eslint-disable no-bitwise */
|
||||||
|
exports.encode = (string: string): ArrayBuffer => {
|
||||||
|
const {length} = string;
|
||||||
|
const bytes = new ByteVector(length);
|
||||||
|
|
||||||
|
// each character / char code is assumed to represent an UTF-16 wchar.
|
||||||
|
// With the notable exception of surrogate pairs, each wchar represents the
|
||||||
|
// corresponding unicode code point.
|
||||||
|
// For an explanation of UTF-8 encoding, read [1]
|
||||||
|
// For an explanation of UTF-16 surrogate pairs, read [2]
|
||||||
|
//
|
||||||
|
// [1] https://en.wikipedia.org/wiki/UTF-8#Description
|
||||||
|
// [2] https://en.wikipedia.org/wiki/UTF-16#U.2B10000_to_U.2B10FFFF
|
||||||
|
let nextCodePoint = string.charCodeAt(0);
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
let codePoint = nextCodePoint;
|
||||||
|
nextCodePoint = string.charCodeAt(i + 1);
|
||||||
|
|
||||||
|
if (codePoint < 0x80) {
|
||||||
|
bytes.push(codePoint);
|
||||||
|
} else if (codePoint < 0x800) {
|
||||||
|
bytes
|
||||||
|
.push(0xc0 | codePoint >>> 6)
|
||||||
|
.push(0x80 | codePoint & 0x3f);
|
||||||
|
} else if (codePoint >>> 10 === 0x36 && nextCodePoint >>> 10 === 0x37) { // high surrogate & low surrogate
|
||||||
|
codePoint = 0x10000 + (((codePoint & 0x3ff) << 10) | (nextCodePoint & 0x3ff));
|
||||||
|
bytes
|
||||||
|
.push(0xf0 | codePoint >>> 18 & 0x7)
|
||||||
|
.push(0x80 | codePoint >>> 12 & 0x3f)
|
||||||
|
.push(0x80 | codePoint >>> 6 & 0x3f)
|
||||||
|
.push(0x80 | codePoint & 0x3f);
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
nextCodePoint = string.charCodeAt(i + 1);
|
||||||
|
} else {
|
||||||
|
bytes
|
||||||
|
.push(0xe0 | codePoint >>> 12)
|
||||||
|
.push(0x80 | codePoint >>> 6 & 0x3f)
|
||||||
|
.push(0x80 | codePoint & 0x3f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes.getBuffer();
|
||||||
|
};
|
||||||
|
|
||||||
|
// align to multiples of 8 bytes
|
||||||
|
function align(size: number): number {
|
||||||
|
return size % 8 ? (Math.floor(size / 8) + 1) << 3 : size;
|
||||||
|
}
|
Loading…
Reference in New Issue