-
Notifications
You must be signed in to change notification settings - Fork 24.9k
[RFC] Add support for missing XHR response types #6870
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f5c6126
d667a6c
673d302
411f590
046800c
f8b10ab
7bf8627
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,14 +13,28 @@ | |
|
|
||
| var RCTNetworking = require('RCTNetworking'); | ||
| 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 OPENED = 1; | ||
| const HEADERS_RECEIVED = 2; | ||
| const LOADING = 3; | ||
| 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. | ||
| */ | ||
|
|
@@ -43,9 +57,7 @@ class XMLHttpRequestBase { | |
| upload: any; | ||
| readyState: number; | ||
| responseHeaders: ?Object; | ||
| responseText: ?string; | ||
| response: ?string; | ||
| responseType: '' | 'text'; | ||
| responseText: string; | ||
| status: number; | ||
| timeout: number; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. get/set properties not yet supported |
||
| responseURL: ?string; | ||
|
|
@@ -57,12 +69,16 @@ class XMLHttpRequestBase { | |
| _requestId: ?number; | ||
| _subscriptions: [any]; | ||
|
|
||
| _method: ?string; | ||
| _url: ?string; | ||
| _headers: Object; | ||
| _sent: boolean; | ||
| _aborted: boolean; | ||
| _cachedResponse: Response; | ||
| _hasError: boolean; | ||
| _headers: Object; | ||
| _lowerCaseResponseHeaders: Object; | ||
| _method: ?string; | ||
| _response: string | ?Object; | ||
| _responseType: ResponseType; | ||
| _sent: boolean; | ||
| _url: ?string; | ||
|
|
||
| constructor() { | ||
| this.UNSENT = UNSENT; | ||
|
|
@@ -82,24 +98,101 @@ class XMLHttpRequestBase { | |
| this._aborted = false; | ||
| } | ||
|
|
||
| _reset() { | ||
| _reset(): void { | ||
| this.readyState = this.UNSENT; | ||
| this.responseHeaders = undefined; | ||
| this.responseText = ''; | ||
| this.response = null; | ||
| this.responseType = ''; | ||
| this.status = 0; | ||
| delete this.responseURL; | ||
|
|
||
| this._requestId = null; | ||
|
|
||
| this._cachedResponse = undefined; | ||
| this._hasError = false; | ||
| this._headers = {}; | ||
| this._responseType = ''; | ||
| this._sent = false; | ||
| this._lowerCaseResponseHeaders = {}; | ||
|
|
||
| this._clearSubscriptions(); | ||
| } | ||
|
|
||
| // $FlowIssue #10784535 | ||
| get responseType(): ResponseType { | ||
| return this._responseType; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. get/set properties not yet supported There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. pity |
||
| } | ||
|
|
||
| // $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 { | ||
| this._requestId = requestId; | ||
| this._subscriptions.push(RCTDeviceEventEmitter.addListener( | ||
|
|
@@ -151,22 +244,7 @@ class XMLHttpRequestBase { | |
| } else { | ||
| this.responseText += responseText; | ||
| } | ||
| switch(this.responseType) { | ||
| 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._cachedResponse = undefined; // force lazy recomputation | ||
| this.setReadyState(this.LOADING); | ||
| } | ||
| } | ||
|
|
@@ -175,6 +253,7 @@ class XMLHttpRequestBase { | |
| if (requestId === this._requestId) { | ||
| if (error) { | ||
| this.responseText = error; | ||
| this._hasError = true; | ||
| } | ||
| this._clearSubscriptions(); | ||
| this._requestId = null; | ||
|
|
@@ -304,4 +383,24 @@ XMLHttpRequestBase.HEADERS_RECEIVED = HEADERS_RECEIVED; | |
| XMLHttpRequestBase.LOADING = LOADING; | ||
| 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; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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])); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I might be missing something here, but doing utf8 ourself doesn't seem like something we should do. Like the #1 rule of crypto "don't roll your own", I think this applies to string encoding too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the complexity of crypto and character encodings is very different. I have been looking for a compact utf-8 encoding lib, but didn’t find anything that was compact and solid at the same time. My implementation adds ~1KB when minified. Do you have any specific concerns? We wouldn’t need to do this if we could pass the binary response data from native to JS wrapped into a host object we’d implement. That host object could pass of character encoding to a system service. I have been looking into JSC’s API for that, but as long as we pass messages as JSON, we can’t do it. |
||
| 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; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How big is this library?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nevermind -_- you implemented it below.