request.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. 'use strict'
  2. const Url = require('url')
  3. const Minipass = require('minipass')
  4. const Headers = require('./headers.js')
  5. const { exportNodeCompatibleHeaders } = Headers
  6. const Body = require('./body.js')
  7. const { clone, extractContentType, getTotalBytes } = Body
  8. const version = require('../package.json').version
  9. const defaultUserAgent =
  10. `minipass-fetch/${version} (+https://github.com/isaacs/minipass-fetch)`
  11. const INTERNALS = Symbol('Request internals')
  12. const { parse: parseUrl, format: formatUrl } = Url
  13. const isRequest = input =>
  14. typeof input === 'object' && typeof input[INTERNALS] === 'object'
  15. const isAbortSignal = signal => {
  16. const proto = (
  17. signal
  18. && typeof signal === 'object'
  19. && Object.getPrototypeOf(signal)
  20. )
  21. return !!(proto && proto.constructor.name === 'AbortSignal')
  22. }
  23. class Request extends Body {
  24. constructor (input, init = {}) {
  25. const parsedURL = isRequest(input) ? Url.parse(input.url)
  26. : input && input.href ? Url.parse(input.href)
  27. : Url.parse(`${input}`)
  28. if (isRequest(input))
  29. init = { ...input[INTERNALS], ...init }
  30. else if (!input || typeof input === 'string')
  31. input = {}
  32. const method = (init.method || input.method || 'GET').toUpperCase()
  33. const isGETHEAD = method === 'GET' || method === 'HEAD'
  34. if ((init.body !== null && init.body !== undefined ||
  35. isRequest(input) && input.body !== null) && isGETHEAD)
  36. throw new TypeError('Request with GET/HEAD method cannot have body')
  37. const inputBody = init.body !== null && init.body !== undefined ? init.body
  38. : isRequest(input) && input.body !== null ? clone(input)
  39. : null
  40. super(inputBody, {
  41. timeout: init.timeout || input.timeout || 0,
  42. size: init.size || input.size || 0,
  43. })
  44. const headers = new Headers(init.headers || input.headers || {})
  45. if (inputBody !== null && inputBody !== undefined &&
  46. !headers.has('Content-Type')) {
  47. const contentType = extractContentType(inputBody)
  48. if (contentType)
  49. headers.append('Content-Type', contentType)
  50. }
  51. const signal = 'signal' in init ? init.signal
  52. : null
  53. if (signal !== null && signal !== undefined && !isAbortSignal(signal))
  54. throw new TypeError('Expected signal must be an instanceof AbortSignal')
  55. // TLS specific options that are handled by node
  56. const {
  57. ca,
  58. cert,
  59. ciphers,
  60. clientCertEngine,
  61. crl,
  62. dhparam,
  63. ecdhCurve,
  64. family,
  65. honorCipherOrder,
  66. key,
  67. passphrase,
  68. pfx,
  69. rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0',
  70. secureOptions,
  71. secureProtocol,
  72. servername,
  73. sessionIdContext,
  74. } = init
  75. this[INTERNALS] = {
  76. method,
  77. redirect: init.redirect || input.redirect || 'follow',
  78. headers,
  79. parsedURL,
  80. signal,
  81. ca,
  82. cert,
  83. ciphers,
  84. clientCertEngine,
  85. crl,
  86. dhparam,
  87. ecdhCurve,
  88. family,
  89. honorCipherOrder,
  90. key,
  91. passphrase,
  92. pfx,
  93. rejectUnauthorized,
  94. secureOptions,
  95. secureProtocol,
  96. servername,
  97. sessionIdContext,
  98. }
  99. // node-fetch-only options
  100. this.follow = init.follow !== undefined ? init.follow
  101. : input.follow !== undefined ? input.follow
  102. : 20
  103. this.compress = init.compress !== undefined ? init.compress
  104. : input.compress !== undefined ? input.compress
  105. : true
  106. this.counter = init.counter || input.counter || 0
  107. this.agent = init.agent || input.agent
  108. }
  109. get method() {
  110. return this[INTERNALS].method
  111. }
  112. get url() {
  113. return formatUrl(this[INTERNALS].parsedURL)
  114. }
  115. get headers() {
  116. return this[INTERNALS].headers
  117. }
  118. get redirect() {
  119. return this[INTERNALS].redirect
  120. }
  121. get signal() {
  122. return this[INTERNALS].signal
  123. }
  124. clone () {
  125. return new Request(this)
  126. }
  127. get [Symbol.toStringTag] () {
  128. return 'Request'
  129. }
  130. static getNodeRequestOptions (request) {
  131. const parsedURL = request[INTERNALS].parsedURL
  132. const headers = new Headers(request[INTERNALS].headers)
  133. // fetch step 1.3
  134. if (!headers.has('Accept'))
  135. headers.set('Accept', '*/*')
  136. // Basic fetch
  137. if (!parsedURL.protocol || !parsedURL.hostname)
  138. throw new TypeError('Only absolute URLs are supported')
  139. if (!/^https?:$/.test(parsedURL.protocol))
  140. throw new TypeError('Only HTTP(S) protocols are supported')
  141. if (request.signal &&
  142. Minipass.isStream(request.body) &&
  143. typeof request.body.destroy !== 'function') {
  144. throw new Error(
  145. 'Cancellation of streamed requests with AbortSignal is not supported')
  146. }
  147. // HTTP-network-or-cache fetch steps 2.4-2.7
  148. const contentLengthValue =
  149. (request.body === null || request.body === undefined) &&
  150. /^(POST|PUT)$/i.test(request.method) ? '0'
  151. : request.body !== null && request.body !== undefined
  152. ? getTotalBytes(request)
  153. : null
  154. if (contentLengthValue)
  155. headers.set('Content-Length', contentLengthValue + '')
  156. // HTTP-network-or-cache fetch step 2.11
  157. if (!headers.has('User-Agent'))
  158. headers.set('User-Agent', defaultUserAgent)
  159. // HTTP-network-or-cache fetch step 2.15
  160. if (request.compress && !headers.has('Accept-Encoding'))
  161. headers.set('Accept-Encoding', 'gzip,deflate')
  162. const agent = typeof request.agent === 'function'
  163. ? request.agent(parsedURL)
  164. : request.agent
  165. if (!headers.has('Connection') && !agent)
  166. headers.set('Connection', 'close')
  167. // TLS specific options that are handled by node
  168. const {
  169. ca,
  170. cert,
  171. ciphers,
  172. clientCertEngine,
  173. crl,
  174. dhparam,
  175. ecdhCurve,
  176. family,
  177. honorCipherOrder,
  178. key,
  179. passphrase,
  180. pfx,
  181. rejectUnauthorized,
  182. secureOptions,
  183. secureProtocol,
  184. servername,
  185. sessionIdContext,
  186. } = request[INTERNALS]
  187. // HTTP-network fetch step 4.2
  188. // chunked encoding is handled by Node.js
  189. return {
  190. ...parsedURL,
  191. method: request.method,
  192. headers: exportNodeCompatibleHeaders(headers),
  193. agent,
  194. ca,
  195. cert,
  196. ciphers,
  197. clientCertEngine,
  198. crl,
  199. dhparam,
  200. ecdhCurve,
  201. family,
  202. honorCipherOrder,
  203. key,
  204. passphrase,
  205. pfx,
  206. rejectUnauthorized,
  207. secureOptions,
  208. secureProtocol,
  209. servername,
  210. sessionIdContext,
  211. }
  212. }
  213. }
  214. module.exports = Request
  215. Object.defineProperties(Request.prototype, {
  216. method: { enumerable: true },
  217. url: { enumerable: true },
  218. headers: { enumerable: true },
  219. redirect: { enumerable: true },
  220. clone: { enumerable: true },
  221. signal: { enumerable: true },
  222. })