lil-http
Version:
Tiny, lightweight, full featured HTTP client
327 lines (283 loc) • 8.46 kB
JavaScript
/*! lil-http - v0.1.17 - MIT License - https://github.com/lil-js/http */
;(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(['exports'], factory)
} else if (typeof exports === 'object') {
factory(exports)
if (typeof module === 'object' && module !== null) {
module.exports = exports = exports.http
}
} else {
factory((root.lil = root.lil || {}))
}
}(this, function (exports) {
'use strict'
var VERSION = '0.1.17'
var toStr = Object.prototype.toString
var slicer = Array.prototype.slice
var hasOwn = Object.prototype.hasOwnProperty
var hasBind = typeof Function.prototype.bind === 'function'
var origin = location.origin
var originRegex = /^(http[s]?:\/\/[a-z0-9\-\.\:]+)[\/]?/i
var jsonMimeRegex = /application\/json/
var hasDomainRequest = typeof XDomainRequest !== 'undefined'
var noop = function () {}
var defaults = {
method: 'GET',
timeout: 30 * 1000,
auth: null,
data: null,
headers: null,
withCredentials: false,
responseType: 'text'
}
function isObj (o) {
return o && toStr.call(o) === '[object Object]' || false
}
function assign (target) {
var i, l, x, cur, args = slicer.call(arguments).slice(1)
for (i = 0, l = args.length; i < l; i += 1) {
cur = args[i]
for (x in cur) if (hasOwn.call(cur, x)) target[x] = cur[x]
}
return target
}
function once (fn) {
var called = false
return function () {
if (called === false) {
called = true
fn.apply(null, arguments)
}
}
}
function setHeaders (xhr, headers) {
if (!isObj(headers)) return
// Set default content type
headers['Content-Type'] = headers['Content-Type'] ||
headers['content-type'] ||
http.defaultContent
var buf = Object.keys(headers).reduce(function (buf, field) {
var lowerField = field.toLowerCase()
// Remove duplicated headers
if (lowerField !== field) {
if (hasOwn.call(headers, lowerField)) {
delete headers[lowerField]
delete buf[lowerField]
}
}
buf[field] = headers[field]
return buf
}, {})
Object.keys(buf).forEach(function (field) {
xhr.setRequestHeader(field, buf[field])
})
}
function getHeaders (xhr) {
var headers = {}, rawHeaders = xhr.getAllResponseHeaders().trim().split('\n')
rawHeaders.forEach(function (header) {
var split = header.trim().split(':')
var key = split.shift().trim()
var value = split.join(':').trim()
headers[key] = value
})
return headers
}
function isJSONResponse (xhr) {
return jsonMimeRegex.test(xhr.getResponseHeader('Content-Type'))
}
function encodeParams (params) {
return Object.getOwnPropertyNames(params).filter(function (name) {
return params[name] !== undefined
}).map(function (name) {
var value = (params[name] === null) ? '' : params[name]
return encodeURIComponent(name) + (value ? '=' + encodeURIComponent(value) : '')
}).join('&').replace(/%20/g, '+')
}
function parseData (xhr) {
var data = null
if (xhr.responseType === 'text') {
data = xhr.responseText
if (isJSONResponse(xhr) && data) data = JSON.parse(data)
} else {
data = xhr.response
}
return data
}
function getStatus (status) {
return status === 1223 ? 204 : status // IE9 fix
}
function buildResponse (xhr) {
var response = {
xhr: xhr,
status: getStatus(xhr.status),
statusText: xhr.statusText,
data: null,
headers: {}
}
if (xhr.readyState === 4) {
response.data = parseData(xhr)
response.headers = getHeaders(xhr)
}
return response
}
function buildErrorResponse (xhr, error) {
var response = buildResponse(xhr)
response.error = error
if (error.stack) response.stack = error.stack
return response
}
function cleanReferences (xhr) {
xhr.onreadystatechange = xhr.onerror = xhr.ontimeout = null
}
function isValidResponseStatus (xhr) {
var status = getStatus(xhr.status)
return status >= 200 && status < 300 || status === 304
}
function onError (xhr, cb) {
return once(function (err) {
cb(buildErrorResponse(xhr, err), null)
})
}
function onLoad (config, xhr, cb) {
return function (ev) {
if (xhr.readyState === 4) {
cleanReferences(xhr)
if (isValidResponseStatus(xhr)) {
cb(null, buildResponse(xhr))
} else {
onError(xhr, cb)(ev)
}
}
}
}
function isCrossOrigin (url) {
var match = url.match(originRegex)
return match && match[1] === origin
}
function getURL (config) {
var url = config.url
if (isObj(config.params)) {
url += (url.indexOf('?') === -1 ? '?' : '&') + encodeParams(config.params)
}
return url
}
function XHRFactory (url) {
if (hasDomainRequest && isCrossOrigin(url)) {
return new XDomainRequest()
} else {
return new XMLHttpRequest()
}
}
function createClient (config) {
var method = (config.method || 'GET').toUpperCase()
var auth = config.auth
var url = getURL(config)
if (!url || typeof url !== 'string') {
throw new TypeError('Missing required request URL')
}
var xhr = XHRFactory(url)
if (auth) {
xhr.open(method, url, true, auth.user, auth.password)
} else {
xhr.open(method, url)
}
xhr.withCredentials = config.withCredentials
xhr.responseType = config.responseType
xhr.timeout = config.timeout
setHeaders(xhr, config.headers)
return xhr
}
function updateProgress (xhr, cb) {
return function (ev) {
if (ev.lengthComputable) {
cb(ev, ev.loaded / ev.total)
} else {
cb(ev)
}
}
}
function hasContentTypeHeader (config) {
return config && isObj(config.headers)
&& (config.headers['content-type'] || config.headers['Content-Type'])
|| false
}
function buildPayload (xhr, config) {
var data = config.data
if (isObj(config.data) || Array.isArray(config.data)) {
if (hasContentTypeHeader(config) === false) {
xhr.setRequestHeader('Content-Type', 'application/json')
}
data = JSON.stringify(config.data)
}
return data
}
function timeoutResolver (cb, timeoutId) {
return function () {
clearTimeout(timeoutId)
cb.apply(null, arguments)
}
}
function request (config, cb, progress) {
var xhr = createClient(config)
var data = buildPayload(xhr, config)
var errorHandler = onError(xhr, cb)
if (hasBind) {
xhr.ontimeout = errorHandler
} else {
var timeoutId = setTimeout(function abort () {
if (xhr.readyState !== 4) {
xhr.abort()
}
}, config.timeout)
cb = timeoutResolver(cb, timeoutId)
errorHandler = onError(xhr, cb)
}
xhr.onreadystatechange = onLoad(config, xhr, cb)
xhr.onerror = errorHandler
if (typeof progress === 'function') {
xhr.onprogress = updateProgress(xhr, progress)
}
try {
xhr.send(data || null)
} catch (e) {
errorHandler(e)
}
return { xhr: xhr, config: config }
}
function requestFactory (method) {
return function (url, options, cb, progress) {
var i, l, cur = null
var config = assign({}, defaults, { method: method })
var args = slicer.call(arguments)
for (i = 0, l = args.length; i < l; i += 1) {
cur = args[i]
if (typeof cur === 'function') {
if (args.length === (i + 1) && typeof args[i - 1] === 'function') {
progress = cur
} else {
cb = cur
}
} else if (isObj(cur)) {
assign(config, cur)
} else if (typeof cur === 'string' && !config.url) {
config.url = cur
}
}
return request(config, cb || noop, progress)
}
}
function http (config, data, cb, progress) {
return requestFactory('GET').apply(null, arguments)
}
http.VERSION = VERSION
http.defaults = defaults
http.defaultContent = 'text/plain'
http.get = requestFactory('GET')
http.post = requestFactory('POST')
http.put = requestFactory('PUT')
http.patch = requestFactory('PATCH')
http.head = requestFactory('HEAD')
http.delete = http.del = requestFactory('DELETE')
return exports.http = http
}))