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