diff --git a/lib/request.js b/lib/request.js index b0a45ce..cfa1e76 100644 --- a/lib/request.js +++ b/lib/request.js @@ -24,16 +24,20 @@ function decideMode (preferBinary) { } var ClientRequest = module.exports = function (opts) { - var self = this - stream.Writable.call(self) + stream.Writable.call(this) - self._opts = opts - self._body = [] - self._headers = {} + this._opts = opts + this._body = [] + this._headers = {} + + ///using _notdestroyed for successfull cleaning in destroy: will set this._notdestroyed to null in order to release variable and will be false of if's .... + this._notdestroyed = true + this._xhr = null + this._response = null if (opts.auth) - self.setHeader('Authorization', 'Basic ' + new Buffer(opts.auth).toString('base64')) + this.setHeader('Authorization', 'Basic ' + new Buffer(opts.auth).toString('base64')) Object.keys(opts.headers).forEach(function (name) { - self.setHeader(name, opts.headers[name]) + this.setHeader(name, opts.headers[name]) }) var preferBinary @@ -50,17 +54,14 @@ var ClientRequest = module.exports = function (opts) { } else { throw new Error('Invalid value for opts.mode') } - self._mode = decideMode(preferBinary) + this._mode = decideMode(preferBinary) - self.on('finish', function () { - self._onFinish() - }) + this.on('finish', this._onFinish.bind(this)); } inherits(ClientRequest, stream.Writable) ClientRequest.prototype.setHeader = function (name, value) { - var self = this var lowerName = name.toLowerCase() // This check is not necessary, but it prevents warnings from browsers about setting unsafe // headers. To be honest I'm not entirely sure hiding these warnings is a good thing, but @@ -68,120 +69,115 @@ ClientRequest.prototype.setHeader = function (name, value) { if (unsafeHeaders.indexOf(lowerName) !== -1) return - self._headers[lowerName] = { + this._headers[lowerName] = { name: name, value: value } } ClientRequest.prototype.getHeader = function (name) { - var self = this - return self._headers[name.toLowerCase()].value + return this._headers[name.toLowerCase()].value } ClientRequest.prototype.removeHeader = function (name) { - var self = this - delete self._headers[name.toLowerCase()] + delete this._headers[name.toLowerCase()] } -ClientRequest.prototype._onFinish = function () { - var self = this +function _toArrayBuffer(buffer) { + return toArrayBuffer(buffer) +} - if (self._destroyed) +function _onFetchSuccess (cr, response){ + cr._fetchResponse = response + cr._connect() +} + +ClientRequest.prototype._onFinish = function () { + if (!this._notdestroyed) return - var opts = self._opts + var opts = this._opts - var headersObj = self._headers + var headersObj = this._headers var body if (opts.method === 'POST' || opts.method === 'PUT' || opts.method === 'PATCH') { if (capability.blobConstructor) { - body = new global.Blob(self._body.map(function (buffer) { - return toArrayBuffer(buffer) - }), { + body = new global.Blob(this._body.map(_toArrayBuffer), { type: (headersObj['content-type'] || {}).value || '' }) } else { // get utf8 string - body = Buffer.concat(self._body).toString() + body = Buffer.concat(this._body).toString() } } - if (self._mode === 'fetch') { + if (this._mode === 'fetch') { var headers = Object.keys(headersObj).map(function (name) { return [headersObj[name].name, headersObj[name].value] }) - global.fetch(self._opts.url, { - method: self._opts.method, + global.fetch(this._opts.url, { + method: this._opts.method, headers: headers, body: body, mode: 'cors', credentials: opts.withCredentials ? 'include' : 'same-origin' - }).then(function (response) { - self._fetchResponse = response - self._connect() - }, function (reason) { - self.emit('error', reason) - }) + }).then(_onFetchSuccess.bind(null, this)), this.emit.bind(this, 'error'); } else { - var xhr = self._xhr = new global.XMLHttpRequest() + var xhr = this._xhr = new global.XMLHttpRequest() try { - xhr.open(self._opts.method, self._opts.url, true) + xhr.open(this._opts.method, this._opts.url, true) } catch (err) { - process.nextTick(function () { - self.emit('error', err) - }) + process.nextTick(this.emit.bind(this, 'error', err)); return } // Can't set responseType on really old browsers if ('responseType' in xhr) - xhr.responseType = self._mode.split(':')[0] + xhr.responseType = this._mode.split(':')[0] if ('withCredentials' in xhr) xhr.withCredentials = !!opts.withCredentials - if (self._mode === 'text' && 'overrideMimeType' in xhr) + if (this._mode === 'text' && 'overrideMimeType' in xhr) xhr.overrideMimeType('text/plain; charset=x-user-defined') Object.keys(headersObj).forEach(function (name) { xhr.setRequestHeader(headersObj[name].name, headersObj[name].value) }) - self._response = null + this._response = null xhr.onreadystatechange = function () { switch (xhr.readyState) { case rStates.LOADING: case rStates.DONE: - self._onXHRProgress() + this._onXHRProgress() break } } // Necessary for streaming in Firefox, since xhr.response is ONLY defined // in onprogress, not in onreadystatechange with xhr.readyState = 3 - if (self._mode === 'moz-chunked-arraybuffer') { - xhr.onprogress = function () { - self._onXHRProgress() - } + if (this._mode === 'moz-chunked-arraybuffer') { + xhr.onprogress = this._onXHRProgress.bind(this); } - xhr.onerror = function () { - if (self._destroyed) - return - self.emit('error', new Error('XHR error')) - } + xhr.onerror = this._onError.bind(this); try { xhr.send(body) } catch (err) { - process.nextTick(function () { - self.emit('error', err) - }) + process.nextTick(this.emit.bind(this, 'error', err)); return } } } +ClientRequest.prototype._onError = function () { + if (!this._notdestroyed) + return + this.emit('error', new Error('XHR error')) + +}; + /** * Checks if xhr.status is readable and non-zero, indicating no error. * Even though the spec says it should be available in readyState 3, @@ -197,53 +193,54 @@ function statusValid (xhr) { } ClientRequest.prototype._onXHRProgress = function () { - var self = this - - if (!statusValid(self._xhr) || self._destroyed) + if (!statusValid(this._xhr) || !this._notdestroyed) return - if (!self._response) - self._connect() + if (!this._response) + this._connect() - self._response._onXHRProgress() + this._response._onXHRProgress() } ClientRequest.prototype._connect = function () { - var self = this - - if (self._destroyed) + if (!this._notdestroyed) return - self._response = new IncomingMessage(self._xhr, self._fetchResponse, self._mode) - self.emit('response', self._response) + this._response = new IncomingMessage(this._xhr, this._fetchResponse, this._mode) + this.emit('response', this._response) } ClientRequest.prototype._write = function (chunk, encoding, cb) { - var self = this - - self._body.push(chunk) + this._body.push(chunk) cb() } ClientRequest.prototype.abort = ClientRequest.prototype.destroy = function () { - var self = this - self._destroyed = true - if (self._response) - self._response._destroyed = true - if (self._xhr) - self._xhr.abort() + this._notdestroyed = null + if (this._response) + this._response._destroyed = true + if (this._xhr) + this._xhr.abort() + +/* + this._body = null; + this._headers = null; + this._xhr = null; + this._response = null; + this._opts = null; + this._mode = null; +*/ // Currently, there isn't a way to truly abort a fetch. // If you like bikeshedding, see https://github.com/whatwg/fetch/issues/27 } ClientRequest.prototype.end = function (data, encoding, cb) { - var self = this if (typeof data === 'function') { cb = data data = undefined } - stream.Writable.prototype.end.call(self, data, encoding, cb) + stream.Writable.prototype.end.call(this, data, encoding, cb) } ClientRequest.prototype.flushHeaders = function () {} diff --git a/lib/response.js b/lib/response.js index b767318..dcc623a 100644 --- a/lib/response.js +++ b/lib/response.js @@ -10,90 +10,103 @@ var rStates = exports.readyStates = { DONE: 4 } +function _onRead(im, reader, result) { + if (im._destroyed) + return + if (result.done) { + im.push(null) + return + } + im.push(new Buffer(result.value)) + read(im, reader) +} + +function read (im, reader) { + reader.read().then(_onRead.bind(null, im, reader)); +} + +function traverseHeaders (im, header) { + var matches = header.match(/^([^:]+):\s*(.*)/) + if (matches) { + var key = matches[1].toLowerCase() + if (key === 'set-cookie') { + if (im.headers[key] === undefined) { + im.headers[key] = [] + } + im.headers[key].push(matches[2]) + } else if (im.headers[key] !== undefined) { + im.headers[key] += ', ' + matches[2] + } else { + im.headers[key] = matches[2] + } + im.rawHeaders.push(matches[1], matches[2]) + } +} + +function onProgress (im, reader) { + if (reader.result.byteLength > im._pos) { + im.push(new Buffer(new Uint8Array(reader.result.slice(im._pos)))) + im._pos = reader.result.byteLength + } +} + +function onLoad (im) { + im.push(null) +} + var IncomingMessage = exports.IncomingMessage = function (xhr, response, mode) { - var self = this - stream.Readable.call(self) + stream.Readable.call(this) - self._mode = mode - self.headers = {} - self.rawHeaders = [] - self.trailers = {} - self.rawTrailers = [] + this._mode = mode + this.headers = {} + this.rawHeaders = [] + this.trailers = {} + this.rawTrailers = [] + this._pos = null; + this._xhr = null; // Fake the 'close' event, but only once 'end' fires - self.on('end', function () { - // The nextTick is necessary to prevent the 'request' module from causing an infinite loop - process.nextTick(function () { - self.emit('close') - }) - }) + // The nextTick is necessary to prevent the 'request' module from causing an infinite loop + this.on('end', process.nextTick.bind(process, this.emit.bind(this, 'close'))) if (mode === 'fetch') { - self._fetchResponse = response + this._fetchResponse = response - self.url = response.url - self.statusCode = response.status - self.statusMessage = response.statusText + this.url = response.url + this.statusCode = response.status + this.statusMessage = response.statusText // backwards compatible version of for ( of ): // for (var ,_i,_it = [Symbol.iterator](); = (_i = _it.next()).value,!_i.done;) for (var header, _i, _it = response.headers[Symbol.iterator](); header = (_i = _it.next()).value, !_i.done;) { - self.headers[header[0].toLowerCase()] = header[1] - self.rawHeaders.push(header[0], header[1]) + this.headers[header[0].toLowerCase()] = header[1] + this.rawHeaders.push(header[0], header[1]) } // TODO: this doesn't respect backpressure. Once WritableStream is available, this can be fixed var reader = response.body.getReader() - function read () { - reader.read().then(function (result) { - if (self._destroyed) - return - if (result.done) { - self.push(null) - return - } - self.push(new Buffer(result.value)) - read() - }) - } - read() + read(this, reader) } else { - self._xhr = xhr - self._pos = 0 + this._xhr = xhr + this._pos = 0 - self.url = xhr.responseURL - self.statusCode = xhr.status - self.statusMessage = xhr.statusText + this.url = xhr.responseURL + this.statusCode = xhr.status + this.statusMessage = xhr.statusText var headers = xhr.getAllResponseHeaders().split(/\r?\n/) - headers.forEach(function (header) { - var matches = header.match(/^([^:]+):\s*(.*)/) - if (matches) { - var key = matches[1].toLowerCase() - if (key === 'set-cookie') { - if (self.headers[key] === undefined) { - self.headers[key] = [] - } - self.headers[key].push(matches[2]) - } else if (self.headers[key] !== undefined) { - self.headers[key] += ', ' + matches[2] - } else { - self.headers[key] = matches[2] - } - self.rawHeaders.push(matches[1], matches[2]) - } - }) + headers.forEach(traverseHeaders.bind(null, this)); - self._charset = 'x-user-defined' + this._charset = 'x-user-defined' if (!capability.overrideMimeType) { - var mimeType = self.rawHeaders['mime-type'] + var mimeType = this.rawHeaders['mime-type'] if (mimeType) { var charsetMatch = mimeType.match(/;\s*charset=([^;])(;|$)/) if (charsetMatch) { - self._charset = charsetMatch[1].toLowerCase() + this._charset = charsetMatch[1].toLowerCase() } } - if (!self._charset) - self._charset = 'utf-8' // best guess + if (!this._charset) + this._charset = 'utf-8' // best guess } } } @@ -103,12 +116,10 @@ inherits(IncomingMessage, stream.Readable) IncomingMessage.prototype._read = function () {} IncomingMessage.prototype._onXHRProgress = function () { - var self = this - - var xhr = self._xhr + var xhr = this._xhr var response = null - switch (self._mode) { + switch (this._mode) { case 'text:vbarray': // For IE9 if (xhr.readyState !== rStates.DONE) break @@ -117,7 +128,7 @@ IncomingMessage.prototype._onXHRProgress = function () { response = new global.VBArray(xhr.responseBody).toArray() } catch (e) {} if (response !== null) { - self.push(new Buffer(response)) + this.push(new Buffer(response)) break } // Falls through in IE8 @@ -125,56 +136,50 @@ IncomingMessage.prototype._onXHRProgress = function () { try { // This will fail when readyState = 3 in IE9. Switch mode and wait for readyState = 4 response = xhr.responseText } catch (e) { - self._mode = 'text:vbarray' + this._mode = 'text:vbarray' break } - if (response.length > self._pos) { - var newData = response.substr(self._pos) - if (self._charset === 'x-user-defined') { + if (response.length > this._pos) { + var newData = response.substr(this._pos) + if (this._charset === 'x-user-defined') { var buffer = new Buffer(newData.length) for (var i = 0; i < newData.length; i++) buffer[i] = newData.charCodeAt(i) & 0xff - self.push(buffer) + this.push(buffer) } else { - self.push(newData, self._charset) + this.push(newData, this._charset) } - self._pos = response.length + this._pos = response.length } break case 'arraybuffer': if (xhr.readyState !== rStates.DONE) break response = xhr.response - self.push(new Buffer(new Uint8Array(response))) + this.push(new Buffer(new Uint8Array(response))) break case 'moz-chunked-arraybuffer': // take whole response = xhr.response if (xhr.readyState !== rStates.LOADING || !response) break - self.push(new Buffer(new Uint8Array(response))) + this.push(new Buffer(new Uint8Array(response))) break case 'ms-stream': response = xhr.response if (xhr.readyState !== rStates.LOADING) break var reader = new global.MSStreamReader() - reader.onprogress = function () { - if (reader.result.byteLength > self._pos) { - self.push(new Buffer(new Uint8Array(reader.result.slice(self._pos)))) - self._pos = reader.result.byteLength - } - } - reader.onload = function () { - self.push(null) - } + reader.onprogress = onProgress.bind(null, this, reader); + reader.onload = onLoad.bind(null, this) + // reader.onerror = ??? // TODO: this reader.readAsArrayBuffer(response) break } // The ms-stream case handles end separately in reader.onload() - if (self._xhr.readyState === rStates.DONE && self._mode !== 'ms-stream') { - self.push(null) + if (this._xhr.readyState === rStates.DONE && this._mode !== 'ms-stream') { + this.push(null) } }