request.js 8.5 KB

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