You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

request.js 7.0KB

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