request.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. var capability = require('./capability')
  2. var inherits = require('inherits')
  3. var response = require('./response')
  4. var stream = require('readable-stream')
  5. var toArrayBuffer = require('to-arraybuffer')
  6. var IncomingMessage = response.IncomingMessage
  7. var rStates = response.readyStates
  8. function decideMode (preferBinary, useFetch) {
  9. if (capability.fetch && useFetch) {
  10. return 'fetch'
  11. } else if (capability.mozchunkedarraybuffer) {
  12. return 'moz-chunked-arraybuffer'
  13. } else if (capability.msstream) {
  14. return 'ms-stream'
  15. } else if (capability.arraybuffer && preferBinary) {
  16. return 'arraybuffer'
  17. } else if (capability.vbArray && preferBinary) {
  18. return 'text:vbarray'
  19. } else {
  20. return 'text'
  21. }
  22. }
  23. var ClientRequest = module.exports = function (opts) {
  24. var self = this
  25. stream.Writable.call(self)
  26. self._opts = opts
  27. self._body = []
  28. self._headers = {}
  29. if (opts.auth)
  30. self.setHeader('Authorization', 'Basic ' + new Buffer(opts.auth).toString('base64'))
  31. Object.keys(opts.headers).forEach(function (name) {
  32. self.setHeader(name, opts.headers[name])
  33. })
  34. var preferBinary
  35. var useFetch = true
  36. if (opts.mode === 'disable-fetch' || 'timeout' in opts) {
  37. // If the use of XHR should be preferred and includes preserving the 'content-type' header.
  38. // Force XHR to be used since the Fetch API does not yet support timeouts.
  39. useFetch = false
  40. preferBinary = true
  41. } else if (opts.mode === 'prefer-streaming') {
  42. // If streaming is a high priority but binary compatibility and
  43. // the accuracy of the 'content-type' header aren't
  44. preferBinary = false
  45. } else if (opts.mode === 'allow-wrong-content-type') {
  46. // If streaming is more important than preserving the 'content-type' header
  47. preferBinary = !capability.overrideMimeType
  48. } else if (!opts.mode || opts.mode === 'default' || opts.mode === 'prefer-fast') {
  49. // Use binary if text streaming may corrupt data or the content-type header, or for speed
  50. preferBinary = true
  51. } else {
  52. throw new Error('Invalid value for opts.mode')
  53. }
  54. self._mode = decideMode(preferBinary, useFetch)
  55. self.on('finish', function () {
  56. self._onFinish()
  57. })
  58. }
  59. inherits(ClientRequest, stream.Writable)
  60. ClientRequest.prototype.setHeader = function (name, value) {
  61. var self = this
  62. var lowerName = name.toLowerCase()
  63. // This check is not necessary, but it prevents warnings from browsers about setting unsafe
  64. // headers. To be honest I'm not entirely sure hiding these warnings is a good thing, but
  65. // http-browserify did it, so I will too.
  66. if (unsafeHeaders.indexOf(lowerName) !== -1)
  67. return
  68. self._headers[lowerName] = {
  69. name: name,
  70. value: value
  71. }
  72. }
  73. ClientRequest.prototype.getHeader = function (name) {
  74. var header = this._headers[name.toLowerCase()]
  75. if (header)
  76. return header.value
  77. return null
  78. }
  79. ClientRequest.prototype.removeHeader = function (name) {
  80. var self = this
  81. delete self._headers[name.toLowerCase()]
  82. }
  83. ClientRequest.prototype._onFinish = function () {
  84. var self = this
  85. if (self._destroyed)
  86. return
  87. var opts = self._opts
  88. var headersObj = self._headers
  89. var body = null
  90. if (opts.method !== 'GET' && opts.method !== 'HEAD') {
  91. if (capability.blobConstructor) {
  92. body = new global.Blob(self._body.map(function (buffer) {
  93. return toArrayBuffer(buffer)
  94. }), {
  95. type: (headersObj['content-type'] || {}).value || ''
  96. })
  97. } else {
  98. // get utf8 string
  99. body = Buffer.concat(self._body).toString()
  100. }
  101. }
  102. // create flattened list of headers
  103. var headersList = []
  104. Object.keys(headersObj).forEach(function (keyName) {
  105. var name = headersObj[keyName].name
  106. var value = headersObj[keyName].value
  107. if (Array.isArray(value)) {
  108. value.forEach(function (v) {
  109. headersList.push([name, v])
  110. })
  111. } else {
  112. headersList.push([name, value])
  113. }
  114. })
  115. if (self._mode === 'fetch') {
  116. global.fetch(self._opts.url, {
  117. method: self._opts.method,
  118. headers: headersList,
  119. body: body || undefined,
  120. mode: 'cors',
  121. credentials: opts.withCredentials ? 'include' : 'same-origin'
  122. }).then(function (response) {
  123. self._fetchResponse = response
  124. self._connect()
  125. }, function (reason) {
  126. self.emit('error', reason)
  127. })
  128. } else {
  129. var xhr = self._xhr = new global.XMLHttpRequest()
  130. try {
  131. xhr.open(self._opts.method, self._opts.url, true)
  132. } catch (err) {
  133. process.nextTick(function () {
  134. self.emit('error', err)
  135. })
  136. return
  137. }
  138. // Can't set responseType on really old browsers
  139. if ('responseType' in xhr)
  140. xhr.responseType = self._mode.split(':')[0]
  141. if ('withCredentials' in xhr)
  142. xhr.withCredentials = !!opts.withCredentials
  143. if (self._mode === 'text' && 'overrideMimeType' in xhr)
  144. xhr.overrideMimeType('text/plain; charset=x-user-defined')
  145. if ('timeout' in opts) {
  146. xhr.timeout = opts.timeout
  147. xhr.ontimeout = function () {
  148. self.emit('timeout')
  149. }
  150. }
  151. headersList.forEach(function (header) {
  152. xhr.setRequestHeader(header[0], header[1])
  153. })
  154. self._response = null
  155. xhr.onreadystatechange = function () {
  156. switch (xhr.readyState) {
  157. case rStates.LOADING:
  158. case rStates.DONE:
  159. self._onXHRProgress()
  160. break
  161. }
  162. }
  163. // Necessary for streaming in Firefox, since xhr.response is ONLY defined
  164. // in onprogress, not in onreadystatechange with xhr.readyState = 3
  165. if (self._mode === 'moz-chunked-arraybuffer') {
  166. xhr.onprogress = function () {
  167. self._onXHRProgress()
  168. }
  169. }
  170. xhr.onerror = function () {
  171. if (self._destroyed)
  172. return
  173. self.emit('error', new Error('XHR error'))
  174. }
  175. try {
  176. xhr.send(body)
  177. } catch (err) {
  178. process.nextTick(function () {
  179. self.emit('error', err)
  180. })
  181. return
  182. }
  183. }
  184. }
  185. /**
  186. * Checks if xhr.status is readable and non-zero, indicating no error.
  187. * Even though the spec says it should be available in readyState 3,
  188. * accessing it throws an exception in IE8
  189. */
  190. function statusValid (xhr) {
  191. try {
  192. var status = xhr.status
  193. return (status !== null && status !== 0)
  194. } catch (e) {
  195. return false
  196. }
  197. }
  198. ClientRequest.prototype._onXHRProgress = function () {
  199. var self = this
  200. if (!statusValid(self._xhr) || self._destroyed)
  201. return
  202. if (!self._response)
  203. self._connect()
  204. self._response._onXHRProgress()
  205. }
  206. ClientRequest.prototype._connect = function () {
  207. var self = this
  208. if (self._destroyed)
  209. return
  210. self._response = new IncomingMessage(self._xhr, self._fetchResponse, self._mode)
  211. self._response.on('error', function(err) {
  212. self.emit('error', err)
  213. })
  214. self.emit('response', self._response)
  215. }
  216. ClientRequest.prototype._write = function (chunk, encoding, cb) {
  217. var self = this
  218. self._body.push(chunk)
  219. cb()
  220. }
  221. ClientRequest.prototype.abort = ClientRequest.prototype.destroy = function () {
  222. var self = this
  223. self._destroyed = true
  224. if (self._response)
  225. self._response._destroyed = true
  226. if (self._xhr)
  227. self._xhr.abort()
  228. // Currently, there isn't a way to truly abort a fetch.
  229. // If you like bikeshedding, see https://github.com/whatwg/fetch/issues/27
  230. }
  231. ClientRequest.prototype.end = function (data, encoding, cb) {
  232. var self = this
  233. if (typeof data === 'function') {
  234. cb = data
  235. data = undefined
  236. }
  237. stream.Writable.prototype.end.call(self, data, encoding, cb)
  238. }
  239. ClientRequest.prototype.flushHeaders = function () {}
  240. ClientRequest.prototype.setTimeout = function () {}
  241. ClientRequest.prototype.setNoDelay = function () {}
  242. ClientRequest.prototype.setSocketKeepAlive = function () {}
  243. // Taken from http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader%28%29-method
  244. var unsafeHeaders = [
  245. 'accept-charset',
  246. 'accept-encoding',
  247. 'access-control-request-headers',
  248. 'access-control-request-method',
  249. 'connection',
  250. 'content-length',
  251. 'cookie',
  252. 'cookie2',
  253. 'date',
  254. 'dnt',
  255. 'expect',
  256. 'host',
  257. 'keep-alive',
  258. 'origin',
  259. 'referer',
  260. 'te',
  261. 'trailer',
  262. 'transfer-encoding',
  263. 'upgrade',
  264. 'user-agent',
  265. 'via'
  266. ]