scoped-http-client
Version:
http client request wrapper
206 lines (170 loc) • 5.47 kB
text/coffeescript
path = require 'path'
http = require 'http'
https = require 'https'
url = require 'url'
qs = require 'querystring'
class ScopedClient
# Those properties are in @options but they are either not passed to the
# request as options or some processing is made on them. They will not be
# added to the request's option param.
@nonPassThroughOptions = ['headers', 'hostname', 'encoding', 'auth', 'port',
'protocol', 'agent', 'query', 'host', 'path', 'pathname', 'slashes', 'hash']
constructor: (url, options) ->
@options = @buildOptions url, options
@passthroughOptions = reduce(extend({}, @options), ScopedClient.nonPassThroughOptions)
request: (method, reqBody, callback) ->
if typeof(reqBody) == 'function'
callback = reqBody
reqBody = null
try
headers = extend {}, @options.headers
sendingData = reqBody and reqBody.length > 0
headers.Host = @options.hostname
# If `callback` is `undefined` it means the caller isn't going to stream
# the body of the request using `callback` and we can set the
# content-length header ourselves.
#
# There is no way to conveniently assert in an else clause because the
# transfer encoding could be chunked or using a newer framing mechanism.
if callback is undefined
headers['Content-Length'] = if sendingData then Buffer.byteLength(reqBody, @options.encoding) else 0
if @options.auth
headers['Authorization'] = 'Basic ' + new Buffer(@options.auth).toString('base64');
port = @options.port ||
ScopedClient.defaultPort[@options.protocol] || 80
requestOptions = {
port: port
host: @options.hostname
method: method
path: @fullPath()
headers: headers
agent: @options.agent
}
# Extends the previous request options with all remaining options
extend requestOptions, @passthroughOptions
req = (if @options.protocol == 'https:' then https else http).request(requestOptions)
if @options.timeout
req.setTimeout @options.timeout, () ->
req.abort()
if callback
req.on 'error', callback
req.write reqBody, @options.encoding if sendingData
callback null, req if callback
catch err
callback err, req if callback
(callback) =>
if callback
req.on 'response', (res) =>
res.setEncoding @options.encoding
body = ''
res.on 'data', (chunk) ->
body += chunk
res.on 'end', ->
callback null, res, body
req.on 'error', (error) ->
callback error, null, null
req.end()
@
# Adds the query string to the path.
fullPath: (p) ->
search = qs.stringify @options.query
full = this.join p
full += "?#{search}" if search.length > 0
full
scope: (url, options, callback) ->
override = @buildOptions url, options
scoped = new ScopedClient(@options)
.protocol(override.protocol)
.host(override.hostname)
.path(override.pathname)
if typeof(url) == 'function'
callback = url
else if typeof(options) == 'function'
callback = options
callback scoped if callback
scoped
join: (suffix) ->
p = @options.pathname || '/'
if suffix and suffix.length > 0
if suffix.match /^\//
suffix
else
path.join p, suffix
else
p
path: (p) ->
@options.pathname = @join p
@
query: (key, value) ->
@options.query ||= {}
if typeof(key) == 'string'
if value
@options.query[key] = value
else
delete @options.query[key]
else
extend @options.query, key
@
host: (h) ->
@options.hostname = h if h and h.length > 0
@
port: (p) ->
if p and (typeof(p) == 'number' || p.length > 0)
@options.port = p
@
protocol: (p) ->
@options.protocol = p if p && p.length > 0
@
encoding: (e = 'utf-8') ->
@options.encoding = e
@
timeout: (time) ->
@options.timeout = time
@
auth: (user, pass) ->
if !user
@options.auth = null
else if !pass and user.match(/:/)
@options.auth = user
else
@options.auth = "#{user}:#{pass}"
@
header: (name, value) ->
@options.headers[name] = value
@
headers: (h) ->
extend @options.headers, h
@
buildOptions: ->
options = {}
i = 0
while arguments[i]
ty = typeof arguments[i]
if ty == 'string'
extend options, url.parse(arguments[i], true)
delete options.url
delete options.href
delete options.search
else if ty != 'function'
extend options, arguments[i]
i += 1
options.headers ||= {}
options.encoding ?= 'utf-8'
options
ScopedClient.methods = ["GET", "POST", "PATCH", "PUT", "DELETE", "HEAD"]
ScopedClient.methods.forEach (method) ->
ScopedClient.prototype[method.toLowerCase()] = (body, callback) ->
@request method, body, callback
ScopedClient.prototype.del = ScopedClient.prototype['delete']
ScopedClient.defaultPort = {'http:':80, 'https:':443, http:80, https:443}
extend = (a, b) ->
Object.keys(b).forEach (prop) ->
a[prop] = b[prop]
a
# Removes keys specified in second parameter from first parameter
reduce = (a, b) ->
for propName in b
delete a[propName]
a
exports.create = (url, options) ->
new ScopedClient url, options