request.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  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' || ('requestTimeout' in opts && !capability.abortController)) {
  37. // If the use of XHR should be preferred. Not typically needed.
  38. useFetch = false
  39. preferBinary = true
  40. } else if (opts.mode === 'prefer-streaming') {
  41. // If streaming is a high priority but binary compatibility and
  42. // the accuracy of the 'content-type' header aren't
  43. preferBinary = false
  44. } else if (opts.mode === 'allow-wrong-content-type') {
  45. // If streaming is more important than preserving the 'content-type' header
  46. preferBinary = !capability.overrideMimeType
  47. } else if (!opts.mode || opts.mode === 'default' || opts.mode === 'prefer-fast') {
  48. // Use binary if text streaming may corrupt data or the content-type header, or for speed
  49. preferBinary = true
  50. } else {
  51. throw new Error('Invalid value for opts.mode')
  52. }
  53. self._mode = decideMode(preferBinary, useFetch)
  54. self._fetchTimer = null
  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.arraybuffer) {
  92. body = toArrayBuffer(Buffer.concat(self._body))
  93. } else if (capability.blobConstructor) {
  94. body = new global.Blob(self._body.map(function (buffer) {
  95. return toArrayBuffer(buffer)
  96. }), {
  97. type: (headersObj['content-type'] || {}).value || ''
  98. })
  99. } else {
  100. // get utf8 string
  101. body = Buffer.concat(self._body).toString()
  102. }
  103. }
  104. // create flattened list of headers
  105. var headersList = []
  106. Object.keys(headersObj).forEach(function (keyName) {
  107. var name = headersObj[keyName].name
  108. var value = headersObj[keyName].value
  109. if (Array.isArray(value)) {
  110. value.forEach(function (v) {
  111. headersList.push([name, v])
  112. })
  113. } else {
  114. headersList.push([name, value])
  115. }
  116. })
  117. if (self._mode === 'fetch') {
  118. var signal = null
  119. var fetchTimer = null
  120. if (capability.abortController) {
  121. var controller = new AbortController()
  122. signal = controller.signal
  123. self._fetchAbortController = controller
  124. if ('requestTimeout' in opts && opts.requestTimeout !== 0) {
  125. self._fetchTimer = global.setTimeout(function () {
  126. self.emit('requestTimeout')
  127. if (self._fetchAbortController)
  128. self._fetchAbortController.abort()
  129. }, opts.requestTimeout)
  130. }
  131. }
  132. global.fetch(self._opts.url, {
  133. method: self._opts.method,
  134. headers: headersList,
  135. body: body || undefined,
  136. mode: 'cors',
  137. credentials: opts.withCredentials ? 'include' : 'same-origin',
  138. signal: signal
  139. }).then(function (response) {
  140. self._fetchResponse = response
  141. self._connect()
  142. }, function (reason) {
  143. global.clearTimeout(self._fetchTimer)
  144. if (!self._destroyed)
  145. self.emit('error', reason)
  146. })
  147. } else {
  148. var xhr = self._xhr = new global.XMLHttpRequest()
  149. try {
  150. xhr.open(self._opts.method, self._opts.url, true)
  151. } catch (err) {
  152. process.nextTick(function () {
  153. self.emit('error', err)
  154. })
  155. return
  156. }
  157. // Can't set responseType on really old browsers
  158. if ('responseType' in xhr)
  159. xhr.responseType = self._mode.split(':')[0]
  160. if ('withCredentials' in xhr)
  161. xhr.withCredentials = !!opts.withCredentials
  162. if (self._mode === 'text' && 'overrideMimeType' in xhr)
  163. xhr.overrideMimeType('text/plain; charset=x-user-defined')
  164. if ('requestTimeout' in opts) {
  165. xhr.timeout = opts.requestTimeout
  166. xhr.ontimeout = function () {
  167. self.emit('requestTimeout')
  168. }
  169. }
  170. headersList.forEach(function (header) {
  171. xhr.setRequestHeader(header[0], header[1])
  172. })
  173. self._response = null
  174. xhr.onreadystatechange = function () {
  175. switch (xhr.readyState) {
  176. case rStates.LOADING:
  177. case rStates.DONE:
  178. self._onXHRProgress()
  179. break
  180. }
  181. }
  182. // Necessary for streaming in Firefox, since xhr.response is ONLY defined
  183. // in onprogress, not in onreadystatechange with xhr.readyState = 3
  184. if (self._mode === 'moz-chunked-arraybuffer') {
  185. xhr.onprogress = function () {
  186. self._onXHRProgress()
  187. }
  188. }
  189. xhr.onerror = function () {
  190. if (self._destroyed)
  191. return
  192. self.emit('error', new Error('XHR error'))
  193. }
  194. try {
  195. xhr.send(body)
  196. } catch (err) {
  197. process.nextTick(function () {
  198. self.emit('error', err)
  199. })
  200. return
  201. }
  202. }
  203. }
  204. /**
  205. * Checks if xhr.status is readable and non-zero, indicating no error.
  206. * Even though the spec says it should be available in readyState 3,
  207. * accessing it throws an exception in IE8
  208. */
  209. function statusValid (xhr) {
  210. try {
  211. var status = xhr.status
  212. return (status !== null && status !== 0)
  213. } catch (e) {
  214. return false
  215. }
  216. }
  217. ClientRequest.prototype._onXHRProgress = function () {
  218. var self = this
  219. if (!statusValid(self._xhr) || self._destroyed)
  220. return
  221. if (!self._response)
  222. self._connect()
  223. self._response._onXHRProgress()
  224. }
  225. ClientRequest.prototype._connect = function () {
  226. var self = this
  227. if (self._destroyed)
  228. return
  229. self._response = new IncomingMessage(self._xhr, self._fetchResponse, self._mode, self._fetchTimer)
  230. self._response.on('error', function(err) {
  231. self.emit('error', err)
  232. })
  233. self.emit('response', self._response)
  234. }
  235. ClientRequest.prototype._write = function (chunk, encoding, cb) {
  236. var self = this
  237. self._body.push(chunk)
  238. cb()
  239. }
  240. ClientRequest.prototype.abort = ClientRequest.prototype.destroy = function () {
  241. var self = this
  242. self._destroyed = true
  243. global.clearTimeout(self._fetchTimer)
  244. if (self._response)
  245. self._response._destroyed = true
  246. if (self._xhr)
  247. self._xhr.abort()
  248. else if (self._fetchAbortController)
  249. self._fetchAbortController.abort()
  250. }
  251. ClientRequest.prototype.end = function (data, encoding, cb) {
  252. var self = this
  253. if (typeof data === 'function') {
  254. cb = data
  255. data = undefined
  256. }
  257. stream.Writable.prototype.end.call(self, data, encoding, cb)
  258. }
  259. ClientRequest.prototype.flushHeaders = function () {}
  260. ClientRequest.prototype.setTimeout = function () {}
  261. ClientRequest.prototype.setNoDelay = function () {}
  262. ClientRequest.prototype.setSocketKeepAlive = function () {}
  263. // Taken from http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader%28%29-method
  264. var unsafeHeaders = [
  265. 'accept-charset',
  266. 'accept-encoding',
  267. 'access-control-request-headers',
  268. 'access-control-request-method',
  269. 'connection',
  270. 'content-length',
  271. 'cookie',
  272. 'cookie2',
  273. 'date',
  274. 'dnt',
  275. 'expect',
  276. 'host',
  277. 'keep-alive',
  278. 'origin',
  279. 'referer',
  280. 'te',
  281. 'trailer',
  282. 'transfer-encoding',
  283. 'upgrade',
  284. 'via'
  285. ]