request.js 4.47 KB
'use strict'

/**
 * request.js
 *
 * Request class contains server only options
 */

const url = require('url')
const Headers = require('./headers.js')
const Body = require('./body.js')
const clone = Body.clone
const extractContentType = Body.extractContentType
const getTotalBytes = Body.getTotalBytes

const PARSED_URL = Symbol('url')

/**
 * Request class
 *
 * @param   Mixed   input  Url or Request instance
 * @param   Object  init   Custom options
 * @return  Void
 */
class Request {
  constructor (input, init) {
    if (!init) init = {}
    let parsedURL

    // normalize input
    if (!(input instanceof Request)) {
      if (input && input.href) {
        // in order to support Node.js' Url objects; though WHATWG's URL objects
        // will fall into this branch also (since their `toString()` will return
        // `href` property anyway)
        parsedURL = url.parse(input.href)
      } else {
        // coerce input to a string before attempting to parse
        parsedURL = url.parse(`${input}`)
      }
      input = {}
    } else {
      parsedURL = url.parse(input.url)
    }

    let method = init.method || input.method || 'GET'

    if ((init.body != null || (input instanceof Request && input.body !== null)) &&
      (method === 'GET' || method === 'HEAD')) {
      throw new TypeError('Request with GET/HEAD method cannot have body')
    }

    let inputBody = init.body != null
      ? init.body
      : input instanceof Request && input.body !== null
        ? clone(input)
        : null

    Body.call(this, inputBody, {
      timeout: init.timeout || input.timeout || 0,
      size: init.size || input.size || 0
    })

    // fetch spec options
    this.method = method.toUpperCase()
    this.redirect = init.redirect || input.redirect || 'follow'
    this.headers = new Headers(init.headers || input.headers || {})

    if (init.body != null) {
      const contentType = extractContentType(this)
      if (contentType !== null && !this.headers.has('Content-Type')) {
        this.headers.append('Content-Type', contentType)
      }
    }

    // server only options
    this.follow = init.follow !== undefined
      ? init.follow : input.follow !== undefined
      ? input.follow : 20
    this.compress = init.compress !== undefined
      ? init.compress : input.compress !== undefined
      ? input.compress : true
    this.counter = init.counter || input.counter || 0
    this.agent = init.agent || input.agent

    this[PARSED_URL] = parsedURL
    Object.defineProperty(this, Symbol.toStringTag, {
      value: 'Request',
      writable: false,
      enumerable: false,
      configurable: true
    })
  }

  get url () {
    return url.format(this[PARSED_URL])
  }

  /**
   * Clone this request
   *
   * @return  Request
   */
  clone () {
    return new Request(this)
  }
}

Body.mixIn(Request.prototype)

Object.defineProperty(Request.prototype, Symbol.toStringTag, {
  value: 'RequestPrototype',
  writable: false,
  enumerable: false,
  configurable: true
})

exports = module.exports = Request

exports.getNodeRequestOptions = function getNodeRequestOptions (request) {
  const parsedURL = request[PARSED_URL]
  const headers = new Headers(request.headers)

  // fetch step 3
  if (!headers.has('Accept')) {
    headers.set('Accept', '*/*')
  }

  // Basic fetch
  if (!parsedURL.protocol || !parsedURL.hostname) {
    throw new TypeError('Only absolute URLs are supported')
  }

  if (!/^https?:$/.test(parsedURL.protocol)) {
    throw new TypeError('Only HTTP(S) protocols are supported')
  }

  // HTTP-network-or-cache fetch steps 5-9
  let contentLengthValue = null
  if (request.body == null && /^(POST|PUT)$/i.test(request.method)) {
    contentLengthValue = '0'
  }
  if (request.body != null) {
    const totalBytes = getTotalBytes(request)
    if (typeof totalBytes === 'number') {
      contentLengthValue = String(totalBytes)
    }
  }
  if (contentLengthValue) {
    headers.set('Content-Length', contentLengthValue)
  }

  // HTTP-network-or-cache fetch step 12
  if (!headers.has('User-Agent')) {
    headers.set('User-Agent', 'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)')
  }

  // HTTP-network-or-cache fetch step 16
  if (request.compress) {
    headers.set('Accept-Encoding', 'gzip,deflate')
  }
  if (!headers.has('Connection') && !request.agent) {
    headers.set('Connection', 'close')
  }

  // HTTP-network fetch step 4
  // chunked encoding is handled by Node.js

  return Object.assign({}, parsedURL, {
    method: request.method,
    headers: headers.raw(),
    agent: request.agent
  })
}