var capability = require('./capability'); var inherits = require('inherits'); var stream = require('readable-stream'); var rStates = (exports.readyStates = { UNSENT: 0, OPENED: 1, HEADERS_RECEIVED: 2, LOADING: 3, DONE: 4 }); var IncomingMessage = (exports.IncomingMessage = function (xhr, response, mode, fetchTimer) { var self = this; stream.Readable.call(self); self._mode = mode; self.headers = {}; self.rawHeaders = []; self.trailers = {}; self.rawTrailers = []; // 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'); }); }); if (mode === 'fetch') { self._fetchResponse = response; self.url = response.url; self.statusCode = response.status; self.statusMessage = response.statusText; response.headers.forEach(function (header, key) { self.headers[key.toLowerCase()] = header; self.rawHeaders.push(key, header); }); if (capability.writableStream) { var writable = new WritableStream({ write: function (chunk) { return new Promise(function (resolve, reject) { if (self._destroyed) { reject(); } else if (self.push(new Buffer(chunk))) { resolve(); } else { self._resumeFetch = resolve; } }); }, close: function () { global.clearTimeout(fetchTimer); if (!self._destroyed) self.push(null); }, abort: function (err) { if (!self._destroyed) self.emit('error', err); } }); try { response.body.pipeTo(writable).catch(function (err) { global.clearTimeout(fetchTimer); if (!self._destroyed) self.emit('error', err); }); return; } catch (e) {} // pipeTo method isn't defined. Can't find a better way to feature test this } // fallback for when writableStream or pipeTo aren't available var reader = response.body.getReader(); function read() { reader .read() .then(function (result) { if (self._destroyed) return; if (result.done) { global.clearTimeout(fetchTimer); self.push(null); return; } self.push(new Buffer(result.value)); read(); }) .catch(function (err) { global.clearTimeout(fetchTimer); if (!self._destroyed) self.emit('error', err); }); } read(); } else { self._xhr = xhr; self._pos = 0; self.url = xhr.responseURL; self.statusCode = xhr.status; self.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]); } }); self._charset = 'x-user-defined'; if (!capability.overrideMimeType) { var mimeType = self.rawHeaders['mime-type']; if (mimeType) { var charsetMatch = mimeType.match(/;\s*charset=([^;])(;|$)/); if (charsetMatch) { self._charset = charsetMatch[1].toLowerCase(); } } if (!self._charset) self._charset = 'utf-8'; // best guess } } }); inherits(IncomingMessage, stream.Readable); IncomingMessage.prototype._read = function () { var self = this; var resolve = self._resumeFetch; if (resolve) { self._resumeFetch = null; resolve(); } }; IncomingMessage.prototype._onXHRProgress = function () { var self = this; var xhr = self._xhr; var response = null; switch (self._mode) { case 'text:vbarray': // For IE9 if (xhr.readyState !== rStates.DONE) break; try { // This fails in IE8 response = new global.VBArray(xhr.responseBody).toArray(); } catch (e) {} if (response !== null) { self.push(new Buffer(response)); break; } // Falls through in IE8 case 'text': 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'; break; } if (response.length > self._pos) { var newData = response.substr(self._pos); if (self._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); } else { self.push(newData, self._charset); } self._pos = response.length; } break; case 'arraybuffer': if (xhr.readyState !== rStates.DONE || !xhr.response) break; response = xhr.response; self.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))); 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.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); } };