react-native-fetch-blob
Version:
A module provides upload, download, and files access API. Supports file stream read/write for process large files.
447 lines (387 loc) • 12.2 kB
JavaScript
// Copyright 2016 wkh237@github. All rights reserved.
// Use of this source code is governed by a MIT-style license that can be
// found in the LICENSE file.
import RNFetchBlob from '../index.js'
import XMLHttpRequestEventTarget from './XMLHttpRequestEventTarget.js'
import Log from '../utils/log.js'
import Blob from './Blob.js'
import ProgressEvent from './ProgressEvent.js'
import URIUtil from '../utils/uri'
const log = new Log('XMLHttpRequest')
log.disable()
// log.level(3)
const UNSENT = 0
const OPENED = 1
const HEADERS_RECEIVED = 2
const LOADING = 3
const DONE = 4
export default class XMLHttpRequest extends XMLHttpRequestEventTarget{
_onreadystatechange : () => void;
upload : XMLHttpRequestEventTarget = new XMLHttpRequestEventTarget();
static binaryContentTypes : Array<string> = [
'image/', 'video/', 'audio/'
];
// readonly
_readyState : number = UNSENT;
_uriType : 'net' | 'file' = 'net';
_response : any = '';
_responseText : any = '';
_responseHeaders : any = {};
_responseType : '' | 'arraybuffer' | 'blob' | 'json' | 'text' = '';
// TODO : not suppoted ATM
_responseURL : null = '';
_responseXML : null = '';
_status : number = 0;
_statusText : string = '';
_timeout : number = 60000;
_sendFlag : boolean = false;
_uploadStarted : boolean = false;
_increment : boolean = false;
// RNFetchBlob compatible data structure
_config : RNFetchBlobConfig = {};
_url : any;
_method : string;
_headers: any = {
'Content-Type' : 'text/plain'
};
_cleanUp : () => void = null;
_body: any;
// RNFetchBlob promise object, which has `progress`, `uploadProgress`, and
// `cancel` methods.
_task: any;
// constants
get UNSENT() { return UNSENT }
get OPENED() { return OPENED }
get HEADERS_RECEIVED() { return HEADERS_RECEIVED }
get LOADING() { return LOADING }
get DONE() { return DONE }
static get UNSENT() {
return UNSENT
}
static get OPENED() {
return OPENED
}
static get HEADERS_RECEIVED() {
return HEADERS_RECEIVED
}
static get LOADING() {
return LOADING
}
static get DONE() {
return DONE
}
static setLog(level:number) {
if(level === -1)
log.disable()
else
log.level(level)
}
static addBinaryContentType(substr:string) {
for(let i in XMLHttpRequest.binaryContentTypes) {
if(new RegExp(substr,'i').test(XMLHttpRequest.binaryContentTypes[i])) {
return
}
}
XMLHttpRequest.binaryContentTypes.push(substr)
}
static removeBinaryContentType(val) {
for(let i in XMLHttpRequest.binaryContentTypes) {
if(new RegExp(substr,'i').test(XMLHttpRequest.binaryContentTypes[i])) {
XMLHttpRequest.binaryContentTypes.splice(i,1)
return
}
}
}
constructor() {
log.verbose('XMLHttpRequest constructor called')
super()
}
/**
* XMLHttpRequest.open, always async, user and password not supported. When
* this method invoked, headers should becomes empty again.
* @param {string} method Request method
* @param {string} url Request URL
* @param {true} async Always async
* @param {any} user NOT SUPPORTED
* @param {any} password NOT SUPPORTED
*/
open(method:string, url:string, async:true, user:any, password:any) {
log.verbose('XMLHttpRequest open ', method, url, async, user, password)
this._method = method
this._url = url
this._headers = {}
this._increment = URIUtil.isJSONStreamURI(this._url)
this._url = this._url.replace(/^JSONStream\:\/\//, '')
this._dispatchReadStateChange(XMLHttpRequest.OPENED)
}
/**
* Invoke this function to send HTTP request, and set body.
* @param {any} body Body in RNfetchblob flavor
*/
send(body) {
this._body = body
if(this._readyState !== XMLHttpRequest.OPENED)
throw 'InvalidStateError : XMLHttpRequest is not opened yet.'
let promise = Promise.resolve()
this._sendFlag = true
log.verbose('XMLHttpRequest send ', body)
let {_method, _url, _headers } = this
log.verbose('sending request with args', _method, _url, _headers, body)
log.verbose(typeof body, body instanceof FormData)
if(body instanceof Blob) {
log.debug('sending blob body', body._blobCreated)
promise = new Promise((resolve, reject) => {
body.onCreated((blob) => {
// when the blob is derived (not created by RN developer), the blob
// will be released after XMLHttpRequest sent
if(blob.isDerived) {
this._cleanUp = () => {
blob.close()
}
}
log.debug('body created send request')
body = RNFetchBlob.wrap(blob.getRNFetchBlobRef())
resolve()
})
})
}
else if(typeof body === 'object') {
body = JSON.stringify(body)
promise = Promise.resolve()
}
else {
body = body ? body.toString() : body
promise = Promise.resolve()
}
promise.then(() => {
log.debug('send request invoke', body)
for(let h in _headers) {
_headers[h] = _headers[h].toString()
}
this._task = RNFetchBlob
.config({
auto: true,
timeout : this._timeout,
increment : this._increment,
binaryContentTypes : XMLHttpRequest.binaryContentTypes
})
.fetch(_method, _url, _headers, body)
this._task
.stateChange(this._headerReceived.bind(this))
.uploadProgress(this._uploadProgressEvent.bind(this))
.progress(this._progressEvent.bind(this))
.catch(this._onError.bind(this))
.then(this._onDone.bind(this))
})
}
overrideMimeType(mime:string) {
log.verbose('XMLHttpRequest overrideMimeType', mime)
this._headers['Content-Type'] = mime
}
setRequestHeader(name, value) {
log.verbose('XMLHttpRequest set header', name, value)
if(this._readyState !== OPENED || this._sendFlag) {
throw `InvalidStateError : Calling setRequestHeader in wrong state ${this._readyState}`
}
// UNICODE SHOULD NOT PASS
if(typeof name !== 'string' || /[^\u0000-\u00ff]/.test(name)) {
throw 'TypeError : header field name should be a string'
}
//
let invalidPatterns = [
/[\(\)\>\<\@\,\:\\\/\[\]\?\=\}\{\s\ \u007f\;\t\0\v\r]/,
/tt/
]
for(let i in invalidPatterns) {
if(invalidPatterns[i].test(name) || typeof name !== 'string') {
throw `SyntaxError : Invalid header field name ${name}`
}
}
this._headers[name] = value
}
abort() {
log.verbose('XMLHttpRequest abort ')
if(!this._task)
return
this._task.cancel((err) => {
let e = {
timeStamp : Date.now(),
}
if(this.onabort)
this.onabort()
if(err) {
e.detail = err
e.type = 'error'
this.dispatchEvent('error', e)
}
else {
e.type = 'abort'
this.dispatchEvent('abort', e)
}
})
}
getResponseHeader(field:string):string | null {
log.verbose('XMLHttpRequest get header', field, this._responseHeaders)
if(!this._responseHeaders)
return null
return (this._responseHeaders[field] || this._responseHeaders[field.toLowerCase()]) || null
}
getAllResponseHeaders():string | null {
log.verbose('XMLHttpRequest get all headers', this._responseHeaders)
if(!this._responseHeaders)
return ''
let result = ''
let respHeaders = this.responseHeaders
for(let i in respHeaders) {
result += `${i}: ${respHeaders[i]}${String.fromCharCode(0x0D,0x0A)}`
}
return result.substr(0, result.length-2)
}
_headerReceived(e) {
log.debug('header received ', this._task.taskId, e)
this.responseURL = this._url
if(e.state === "2") {
this._responseHeaders = e.headers
this._statusText = e.status
this._status = Math.floor(e.status)
this._dispatchReadStateChange(XMLHttpRequest.HEADERS_RECEIVED)
}
}
_uploadProgressEvent(send:number, total:number) {
if(!this._uploadStarted) {
this.upload.dispatchEvent('loadstart')
this._uploadStarted = true
}
if(send >= total)
this.upload.dispatchEvent('load')
this.upload.dispatchEvent('progress', new ProgressEvent(true, send, total))
}
_progressEvent(send:number, total:number, chunk:string) {
log.verbose(this.readyState)
if(this._readyState === XMLHttpRequest.HEADERS_RECEIVED)
this._dispatchReadStateChange(XMLHttpRequest.LOADING)
let lengthComputable = false
if(total && total >= 0)
lengthComputable = true
let e = new ProgressEvent(lengthComputable, send, total)
if(this._increment) {
this._responseText += chunk
}
this.dispatchEvent('progress', e)
}
_onError(err) {
let statusCode = Math.floor(this.status)
if(statusCode >= 100 && statusCode !== 408) {
return
}
log.debug('XMLHttpRequest error', err)
this._statusText = err
this._status = String(err).match(/\d+/)
this._status = this._status ? Math.floor(this.status) : 404
this._dispatchReadStateChange(XMLHttpRequest.DONE)
if(err && String(err.message).match(/(timed\sout|timedout)/) || this._status == 408) {
this.dispatchEvent('timeout')
}
this.dispatchEvent('loadend')
this.dispatchEvent('error', {
type : 'error',
detail : err
})
this.clearEventListeners()
}
_onDone(resp) {
log.debug('XMLHttpRequest done', this._url, resp, this)
this._statusText = this._status
let responseDataReady = () => {
log.debug('request done state = 4')
this.dispatchEvent('load')
this.dispatchEvent('loadend')
this._dispatchReadStateChange(XMLHttpRequest.DONE)
this.clearEventListeners()
}
if(resp) {
let info = resp.respInfo || {}
log.debug(this._url, info, info.respType)
switch(this._responseType) {
case 'blob' :
resp.blob().then((b) => {
this._responseText = resp.text()
this._response = b
responseDataReady()
})
break;
case 'arraybuffer':
// TODO : to array buffer
break
case 'json':
this._response = resp.json()
this._responseText = resp.text()
break
default :
this._responseText = resp.text()
this._response = this.responseText
responseDataReady()
break;
}
}
}
_dispatchReadStateChange(state) {
this._readyState = state
if(typeof this._onreadystatechange === 'function')
this._onreadystatechange()
}
set onreadystatechange(fn:() => void) {
log.verbose('XMLHttpRequest set onreadystatechange', fn)
this._onreadystatechange = fn
}
get onreadystatechange() {
return this._onreadystatechange
}
get readyState() {
log.verbose('get readyState', this._readyState)
return this._readyState
}
get status() {
log.verbose('get status', this._status)
return this._status
}
get statusText() {
log.verbose('get statusText', this._statusText)
return this._statusText
}
get response() {
log.verbose('get response', this._response)
return this._response
}
get responseText() {
log.verbose('get responseText', this._responseText)
return this._responseText
}
get responseURL() {
log.verbose('get responseURL', this._responseURL)
return this._responseURL
}
get responseHeaders() {
log.verbose('get responseHeaders', this._responseHeaders)
return this._responseHeaders
}
set timeout(val) {
this._timeout = val*1000
log.verbose('set timeout', this._timeout)
}
get timeout() {
log.verbose('get timeout', this._timeout)
return this._timeout
}
set responseType(val) {
log.verbose('set response type', this._responseType)
this._responseType = val
}
get responseType() {
log.verbose('get response type', this._responseType)
return this._responseType
}
static get isRNFBPolyfill() {
return true
}
}