najaxjs/najax

View on GitHub
lib/najax.js

Summary

Maintainability
D
2 days
Test Coverage
/* najax
 * jquery ajax-stye http requests in node
 * https://github.com/alanclarke/najax
 */

var https = require('https')
var http = require('http')
var querystring = require('qs')
var url = require('url')
var zlib = require('zlib')
var $ = require('jquery-deferred')
var defaultsDeep = require('lodash/defaultsDeep')
var parseOptions = require('./parse-options')
var defaults = {
  method: 'GET',
  rejectUnauthorized: true,
  processData: true,
  data: '',
  contentType: 'application/x-www-form-urlencoded',
  headers: {},
  setRequestHeader: function (name, value) {
    this.headers[name] = value
  }
}

/*
  method overloading, can use:
  -function(url, opts, callback) or
  -function(url, callback)
  -function(opts)
*/
function najax (uri, options, callback) {
  var dfd = new $.Deferred()
  var o = defaultsDeep({}, parseOptions(uri, options, callback), defaults)
  var l = url.parse(o.url)
  var ssl = l.protocol.indexOf('https') === 0

  // DATA
  // Per jquery docs / source: encoding is only done
  // if processData is true (defaults to true)
  // and the data is not already a string
  // https://github.com/jquery/jquery/blob/master/src/ajax.js#L518
  if (o.data && o.processData && o.method === 'GET') {
    o.data = querystring.stringify(o.data, { arrayFormat: 'brackets' })
  } else if (
    o.data &&
    o.processData &&
    typeof o.data !== 'string' &&
    o.method !== 'GET'
  ) {
    switch (true) {
      case o.contentType.startsWith('application/json'):
        o.data = JSON.stringify(o.data)
        break
      case o.contentType.startsWith('application/x-www-form-urlencoded'):
        o.data = querystring.stringify(o.data)
        break
      default:
        o.data = String(o.data)
    }
  }

  /* if get, use querystring method for data */
  if (o.data) {
    if (o.method === 'GET') {
      if (l.search) {
        l.search += '&' + o.data
      } else {
        l.search = '?' + o.data
      }
    } else {
      /* set data content type */
      o.headers = Object.assign(
        {
          'Content-Type': o.contentType,
          'Content-Length': Buffer.byteLength(o.data)
        },
        o.headers
      )
    }
  }

  if (o.beforeSend) o.beforeSend(o)

  options = {
    host: l.hostname,
    path: l.pathname + (l.search || ''),
    method: o.method,
    port: Number(l.port) || (ssl ? 443 : 80),
    headers: o.headers,
    rejectUnauthorized: o.rejectUnauthorized
  }

  // AUTHENTICATION
  /* add authentication to http request */
  if (l.auth) {
    options.auth = l.auth
  } else if (o.username && o.password) {
    options.auth = o.username + ':' + o.password
  } else if (o.auth) {
    options.auth = o.auth
  }
  /* pass keep-alive agent if provided */
  if (o.agent) options.agent = o.agent

  /* for debugging, method to get options and return */
  if (o.getopts) {
    var getopts = [
      ssl,
      options,
      o.data || false,
      o.success || false,
      o.error || false
    ]
    return getopts
  }

  // REQUEST
  function notImplemented (name) {
    return function () {
      console.error('najax: method jqXHR."' + name + '" not implemented')
      console.trace()
    }
  }

  var jqXHR = {
    readyState: 0,
    status: 0,
    statusText: 'error', // one of: "success", "notmodified", "error", "timeout", "abort", or "parsererror"
    setRequestHeader: notImplemented('setRequestHeader'),
    getAllResponseHeaders: notImplemented('getAllResponseHeaders'),
    statusCode: notImplemented('statusCode'),
    abort: notImplemented('abort')
  }

  var req = (ssl ? https : http).request(options, function (res) {
    // Allow getting Response Headers from the XMLHTTPRequest object
    dfd.getResponseHeader = jqXHR.getResponseHeader = function getResponseHeader (
      header
    ) {
      return res.headers[header.toLowerCase()]
    }
    dfd.getAllResponseHeaders = jqXHR.getAllResponseHeaders = function getAllResponseHeaders () {
      var headers = []
      for (var key in res.headers) {
        headers.push(key + ': ' + res.headers[key])
      }
      return headers.join('\n')
    }

    function dataHandler (data) {
      jqXHR.responseText = data

      var statusCode = res.statusCode
      //
      // Determine if successful
      // (per https://github.com/jquery/jquery/blob/master/src/ajax.js#L679)
      var isSuccess =
        (statusCode >= 200 && statusCode < 300) || statusCode === 304
      // Set readyState
      jqXHR.readyState = statusCode > 0 ? 4 : 0
      jqXHR.status = statusCode

      if (o.dataType === 'json' || o.dataType === 'jsonp') {
        // replace control characters
        try {
          data = JSON.parse(data.replace(/[\cA-\cZ]/gi, ''))
        } catch (e) {
          jqXHR.statusText = 'parseerror'
          return onError(e)
        }
      }

      if (isSuccess) {
        jqXHR.statusText = 'success'

        if (statusCode === 204 || options.method === 'HEAD') {
          jqXHR.statusText = 'nocontent'
        } else if (statusCode === 304) {
          jqXHR.statusText = 'notmodified'
        }

        // success, statusText, jqXHR
        dfd.resolve(data, jqXHR.statusText, jqXHR)
      } else {
        // jqXHR, statusText, error
        // When an HTTP error occurs, errorThrown receives the textual portion of the
        // HTTP status, such as "Not Found" or "Internal Server Error."
        jqXHR.statusText = 'error'
        onError(new Error(http.STATUS_CODES[statusCode]))
      }
    }
    var chunks = []
    res.on('data', function (chunk) {
      chunks.push(chunk)
    })
    res.on('end', function () {
      var buffer = Buffer.concat(chunks)
      var encoding = res.headers['content-encoding']
      if (encoding === 'gzip') {
        zlib.gunzip(buffer, function (err, buffer) {
          if (err) {
            onError(err)
          } else {
            dataHandler(buffer.toString())
          }
        })
      } else if (encoding === 'deflate') {
        zlib.inflate(buffer, function (err, buffer) {
          if (err) {
            onError(err)
          } else {
            dataHandler(buffer.toString())
          }
        })
      } else {
        dataHandler(buffer.toString())
      }
    })
  })

  // ERROR
  req.on('error', onError)

  function onError (e) {
    // jqXHR, statusText, error
    dfd.reject(jqXHR, jqXHR.statusText, e)
  }

  // SET TIMEOUT
  if (o.timeout && o.timeout > 0) {
    req.setTimeout(o.timeout, function () {
      req.abort()
      jqXHR.statusText = 'timeout'
      onError(new Error('timeout'))
    })
  }

  // SEND DATA
  if (o.method !== 'GET' && o.data) req.write(o.data, 'utf-8')
  req.end()

  // DEFERRED
  dfd.done(o.success)
  dfd.done(o.complete)
  dfd.fail(o.error)
  dfd.fail(o.complete)
  dfd.success = dfd.done
  dfd.error = dfd.fail
  return dfd
}

najax.defaults = function mergeDefaults (opts) {
  return defaultsDeep(defaults, opts)
}

/* auto rest interface go! */
;['GET', 'POST', 'PUT', 'DELETE'].forEach(handleMethod)

function handleMethod (method) {
  najax[method.toLowerCase()] = function methodHandler (
    uri,
    options,
    callback
  ) {
    return najax(
      defaultsDeep(parseOptions(uri, options, callback), { method: method })
    )
  }
}

module.exports = najax