1 // Copyright 2010-2012 Mikeal Rogers
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
7 // http://www.apache.org/licenses/LICENSE-2.0
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
15 var http = require('http')
18 , url = require('url')
19 , util = require('util')
20 , stream = require('stream')
21 , qs = require('querystring')
22 , oauth = require('./oauth')
23 , uuid = require('./uuid')
24 , ForeverAgent = require('./forever')
25 , Cookie = require('./vendor/cookie')
26 , CookieJar = require('./vendor/cookie/jar')
27 , cookieJar = new CookieJar
28 , tunnel = require('./tunnel')
29 , aws = require('./aws')
31 , mime = require('mime')
32 , FormData = require('form-data')
35 if (process.logging) {
36 var log = process.logging('request')
40 https = require('https')
47 function toBase64 (str) {
48 return (new Buffer(str || "", "ascii")).toString("base64")
51 // Hacky fix for pre-0.4.4 https
52 if (https && !https.Agent) {
53 https.Agent = function (options) {
54 http.Agent.call(this, options)
56 util.inherits(https.Agent, http.Agent)
57 https.Agent.prototype._getConnection = function (host, port, cb) {
58 var s = tls.connect(port, host, this.options, function () {
59 // do other checks here?
66 function isReadStream (rs) {
67 if (rs.readable && rs.path && rs.mode) {
74 Object.keys(obj).forEach(function (i) {
80 var isUrl = /^https?:/
84 function Request (options) {
85 stream.Stream.call(this)
89 if (typeof options === 'string') {
90 options = {uri:options}
93 var reserved = Object.keys(Request.prototype)
94 for (var i in options) {
95 if (reserved.indexOf(i) === -1) {
98 if (typeof options[i] === 'function') {
103 options = copy(options)
107 util.inherits(Request, stream.Stream)
108 Request.prototype.init = function (options) {
111 if (!options) options = {}
112 if (process.env.NODE_DEBUG && /request/.test(process.env.NODE_DEBUG)) console.error('REQUEST', options)
113 if (!self.pool && self.pool !== false) self.pool = globalPool
115 self.__isRequestRequest = true
117 // Protect against double callback
118 if (!self._callback && self.callback) {
119 self._callback = self.callback
120 self.callback = function () {
121 if (self._callbackCalled) return // Print a warning maybe?
122 self._callback.apply(self, arguments)
123 self._callbackCalled = true
125 self.on('error', self.callback.bind())
126 self.on('complete', self.callback.bind(self, null))
130 // People use this property instead all the time so why not just support it.
136 // this will throw if unhandled but is handleable when in a redirect
137 return self.emit('error', new Error("options.uri is a required argument"))
139 if (typeof self.uri == "string") self.uri = url.parse(self.uri)
142 if (typeof self.proxy == 'string') self.proxy = url.parse(self.proxy)
144 // do the HTTP CONNECT dance using koichik/node-tunnel
145 if (http.globalAgent && self.uri.protocol === "https:") {
146 var tunnelFn = self.proxy.protocol === "http:"
147 ? tunnel.httpsOverHttp : tunnel.httpsOverHttps
149 var tunnelOptions = { proxy: { host: self.proxy.hostname
150 , port: +self.proxy.port
151 , proxyAuth: self.proxy.auth }
154 self.agent = tunnelFn(tunnelOptions)
159 if (!self.uri.host || !self.uri.pathname) {
160 // Invalid URI: it may generate lot of bad errors, like "TypeError: Cannot call method 'indexOf' of undefined" in CookieJar
161 // Detect and reject it as soon as possible
162 var faultyUri = url.format(self.uri)
163 var message = 'Invalid URI "' + faultyUri + '"'
164 if (Object.keys(options).length === 0) {
165 // No option ? This can be the sign of a redirect
166 // As this is a case where the user cannot do anything (he didn't call request directly with this URL)
167 // he should be warned that it can be caused by a redirection (can save some hair)
168 message += '. This can be caused by a crappy redirection.'
170 self.emit('error', new Error(message))
171 return // This error was fatal
174 self._redirectsFollowed = self._redirectsFollowed || 0
175 self.maxRedirects = (self.maxRedirects !== undefined) ? self.maxRedirects : 10
176 self.followRedirect = (self.followRedirect !== undefined) ? self.followRedirect : true
177 self.followAllRedirects = (self.followAllRedirects !== undefined) ? self.followAllRedirects : false
178 if (self.followRedirect || self.followAllRedirects)
179 self.redirects = self.redirects || []
181 self.headers = self.headers ? copy(self.headers) : {}
184 if (!self.headers.host) {
185 self.headers.host = self.uri.hostname
187 if ( !(self.uri.port === 80 && self.uri.protocol === 'http:') &&
188 !(self.uri.port === 443 && self.uri.protocol === 'https:') )
189 self.headers.host += (':'+self.uri.port)
194 self.jar(self._jar || options.jar)
196 if (!self.uri.pathname) {self.uri.pathname = '/'}
197 if (!self.uri.port) {
198 if (self.uri.protocol == 'http:') {self.uri.port = 80}
199 else if (self.uri.protocol == 'https:') {self.uri.port = 443}
202 if (self.proxy && !self.tunnel) {
203 self.port = self.proxy.port
204 self.host = self.proxy.hostname
206 self.port = self.uri.port
207 self.host = self.uri.hostname
210 self.clientErrorHandler = function (error) {
211 if (self._aborted) return
213 if (self.setHost) delete self.headers.host
214 if (self.req._reusedSocket && error.code === 'ECONNRESET'
215 && self.agent.addRequestNoreuse) {
216 self.agent = { addRequest: self.agent.addRequestNoreuse.bind(self.agent) }
221 if (self.timeout && self.timeoutTimer) {
222 clearTimeout(self.timeoutTimer)
223 self.timeoutTimer = null
225 self.emit('error', error)
228 self._parserErrorHandler = function (error) {
230 if (this.res.request) {
231 this.res.request.emit('error', error)
233 this.res.emit('error', error)
236 this._httpMessage.emit('error', error)
241 self.form(options.form)
245 self.oauth(options.oauth)
249 self.aws(options.aws)
252 if (self.uri.auth && !self.headers.authorization) {
253 self.headers.authorization = "Basic " + toBase64(self.uri.auth.split(':').map(function(item){ return qs.unescape(item)}).join(':'))
255 if (self.proxy && self.proxy.auth && !self.headers['proxy-authorization'] && !self.tunnel) {
256 self.headers['proxy-authorization'] = "Basic " + toBase64(self.proxy.auth.split(':').map(function(item){ return qs.unescape(item)}).join(':'))
259 if (options.qs) self.qs(options.qs)
262 self.path = self.uri.path
264 self.path = self.uri.pathname + (self.uri.search || "")
267 if (self.path.length === 0) self.path = '/'
269 if (self.proxy && !self.tunnel) self.path = (self.uri.protocol + '//' + self.uri.host + self.path)
272 self.json(options.json)
273 } else if (options.multipart) {
274 self.boundary = uuid()
275 self.multipart(options.multipart)
280 if (!Buffer.isBuffer(self.body)) {
281 if (Array.isArray(self.body)) {
282 for (var i = 0; i < self.body.length; i++) {
283 length += self.body[i].length
286 self.body = new Buffer(self.body)
287 length = self.body.length
290 length = self.body.length
293 if(!self.headers['content-length'] && !self.headers['Content-Length'])
294 self.headers['content-length'] = length
296 throw new Error('Argument error, options.body.')
300 var protocol = self.proxy && !self.tunnel ? self.proxy.protocol : self.uri.protocol
301 , defaultModules = {'http:':http, 'https:':https}
302 , httpModules = self.httpModules || {}
304 self.httpModule = httpModules[protocol] || defaultModules[protocol]
306 if (!self.httpModule) return this.emit('error', new Error("Invalid protocol"))
308 if (options.ca) self.ca = options.ca
311 if (options.agentOptions) self.agentOptions = options.agentOptions
313 if (options.agentClass) {
314 self.agentClass = options.agentClass
315 } else if (options.forever) {
316 self.agentClass = protocol === 'http:' ? ForeverAgent : ForeverAgent.SSL
318 self.agentClass = self.httpModule.Agent
322 if (self.pool === false) {
325 self.agent = self.agent || self.getAgent()
326 if (self.maxSockets) {
327 // Don't use our pooling if node has the refactored client
328 self.agent.maxSockets = self.maxSockets
330 if (self.pool.maxSockets) {
331 // Don't use our pooling if node has the refactored client
332 self.agent.maxSockets = self.pool.maxSockets
336 self.once('pipe', function (src) {
337 if (self.ntick && self._started) throw new Error("You cannot pipe to this stream after the outbound request has started.")
339 if (isReadStream(src)) {
340 if (!self.headers['content-type'] && !self.headers['Content-Type'])
341 self.headers['content-type'] = mime.lookup(src.path)
344 for (var i in src.headers) {
345 if (!self.headers[i]) {
346 self.headers[i] = src.headers[i]
350 if (self._json && !self.headers['content-type'] && !self.headers['Content-Type'])
351 self.headers['content-type'] = 'application/json'
352 if (src.method && !self.method) {
353 self.method = src.method
357 self.on('pipe', function () {
358 console.error("You have already piped to this stream. Pipeing twice is likely to break the request.")
362 process.nextTick(function () {
363 if (self._aborted) return
366 self.setHeaders(self._form.getHeaders())
367 self._form.pipe(self)
370 if (Array.isArray(self.body)) {
371 self.body.forEach(function (part) {
375 self.write(self.body)
378 } else if (self.requestBodyStream) {
379 console.warn("options.requestBodyStream is deprecated, please pass the request object to stream.pipe.")
380 self.requestBodyStream.pipe(self)
381 } else if (!self.src) {
382 if (self.method !== 'GET' && typeof self.method !== 'undefined') {
383 self.headers['content-length'] = 0
391 // Must call this when following a redirect from https to http or vice versa
392 // Attempts to keep everything as identical as possible, but update the
393 // httpModule, Tunneling agent, and/or Forever Agent in use.
394 Request.prototype._updateProtocol = function () {
396 var protocol = self.uri.protocol
398 if (protocol === 'https:') {
399 // previously was doing http, now doing https
400 // if it's https, then we might need to tunnel now.
403 var tunnelFn = self.proxy.protocol === 'http:'
404 ? tunnel.httpsOverHttp : tunnel.httpsOverHttps
405 var tunnelOptions = { proxy: { host: self.proxy.hostname
406 , post: +self.proxy.port
407 , proxyAuth: self.proxy.auth }
409 self.agent = tunnelFn(tunnelOptions)
413 self.httpModule = https
414 switch (self.agentClass) {
416 self.agentClass = ForeverAgent.SSL
419 self.agentClass = https.Agent
422 // nothing we can do. Just hope for the best.
426 // if there's an agent, we need to get a new one.
427 if (self.agent) self.agent = self.getAgent()
430 if (log) log('previously https, now http')
431 // previously was doing https, now doing http
432 // stop any tunneling.
433 if (self.tunnel) self.tunnel = false
434 self.httpModule = http
435 switch (self.agentClass) {
436 case ForeverAgent.SSL:
437 self.agentClass = ForeverAgent
440 self.agentClass = http.Agent
443 // nothing we can do. just hope for the best
447 // if there's an agent, then get a new one.
450 self.agent = self.getAgent()
455 Request.prototype.getAgent = function () {
456 var Agent = this.agentClass
458 if (this.agentOptions) {
459 for (var i in this.agentOptions) {
460 options[i] = this.agentOptions[i]
463 if (this.ca) options.ca = this.ca
467 // different types of agents are in different pools
468 if (Agent !== this.httpModule.Agent) {
469 poolKey += Agent.name
472 if (!this.httpModule.globalAgent) {
474 options.host = this.host
475 options.port = this.port
476 if (poolKey) poolKey += ':'
477 poolKey += this.host + ':' + this.port
480 // ca option is only relevant if proxy or destination are https
481 var proxy = this.proxy
482 if (typeof proxy === 'string') proxy = url.parse(proxy)
483 var caRelevant = (proxy && proxy.protocol === 'https:') || this.uri.protocol === 'https:'
484 if (options.ca && caRelevant) {
485 if (poolKey) poolKey += ':'
486 poolKey += options.ca
489 if (!poolKey && Agent === this.httpModule.Agent && this.httpModule.globalAgent) {
490 // not doing anything special. Use the globalAgent
491 return this.httpModule.globalAgent
494 // we're using a stored agent. Make sure it's protocol-specific
495 poolKey = this.uri.protocol + poolKey
497 // already generated an agent for this setting
498 if (this.pool[poolKey]) return this.pool[poolKey]
500 return this.pool[poolKey] = new Agent(options)
503 Request.prototype.start = function () {
506 if (self._aborted) return
509 self.method = self.method || 'GET'
510 self.href = self.uri.href
511 if (log) log('%method %href', self)
513 if (self.src && self.src.stat && self.src.stat.size && !self.headers['content-length'] && !self.headers['Content-Length']) {
514 self.headers['content-length'] = self.src.stat.size
517 self.aws(self._aws, true)
519 self.req = self.httpModule.request(self, function (response) {
520 if (response.connection.listeners('error').indexOf(self._parserErrorHandler) === -1) {
521 response.connection.once('error', self._parserErrorHandler)
523 if (self._aborted) return
524 if (self._paused) response.pause()
526 self.response = response
527 response.request = self
528 response.toJSON = toJSON
530 if (self.httpModule === https &&
532 !response.client.authorized) {
533 var sslErr = response.client.authorizationError
534 self.emit('error', new Error('SSL Error: '+ sslErr))
538 if (self.setHost) delete self.headers.host
539 if (self.timeout && self.timeoutTimer) {
540 clearTimeout(self.timeoutTimer)
541 self.timeoutTimer = null
544 var addCookie = function (cookie) {
545 if (self._jar) self._jar.add(new Cookie(cookie))
546 else cookieJar.add(new Cookie(cookie))
549 if (response.headers['set-cookie'] && (!self._disableCookies)) {
550 if (Array.isArray(response.headers['set-cookie'])) response.headers['set-cookie'].forEach(addCookie)
551 else addCookie(response.headers['set-cookie'])
554 if (response.statusCode >= 300 && response.statusCode < 400 &&
555 (self.followAllRedirects ||
556 (self.followRedirect && (self.method !== 'PUT' && self.method !== 'POST' && self.method !== 'DELETE'))) &&
557 response.headers.location) {
558 if (self._redirectsFollowed >= self.maxRedirects) {
559 self.emit('error', new Error("Exceeded maxRedirects. Probably stuck in a redirect loop "+self.uri.href))
562 self._redirectsFollowed += 1
564 if (!isUrl.test(response.headers.location)) {
565 response.headers.location = url.resolve(self.uri.href, response.headers.location)
568 var uriPrev = self.uri
569 self.uri = url.parse(response.headers.location)
571 // handle the case where we change protocol from https to http or vice versa
572 if (self.uri.protocol !== uriPrev.protocol) {
573 self._updateProtocol()
577 { statusCode : response.statusCode
578 , redirectUri: response.headers.location
581 if (self.followAllRedirects) self.method = 'GET'
582 // self.method = 'GET' // Force all redirects to use GET || commented out fixes #215
590 delete self.headers.host
591 delete self.headers['content-type']
592 delete self.headers['content-length']
594 if (log) log('Redirect to %uri', self)
596 return // Ignore the rest of the response
598 self._redirectsFollowed = self._redirectsFollowed || 0
599 // Be a good stream and emit end when the response is finished.
600 // Hack to emit end on close because of a core bug that never fires end
601 response.on('close', function () {
602 if (!self._ended) self.response.emit('end')
606 if (self.dests.length !== 0) {
607 console.error("Ingoring encoding parameter as this stream is being piped to another stream which makes the encoding option invalid.")
609 response.setEncoding(self.encoding)
613 self.dests.forEach(function (dest) {
617 response.on("data", function (chunk) {
618 self._destdata = true
619 self.emit("data", chunk)
621 response.on("end", function (chunk) {
623 self.emit("end", chunk)
625 response.on("close", function () {self.emit("close")})
627 self.emit('response', response)
632 self.on("data", function (chunk) {
634 bodyLen += chunk.length
636 self.on("end", function () {
637 if (self._aborted) return
639 if (buffer.length && Buffer.isBuffer(buffer[0])) {
640 var body = new Buffer(bodyLen)
642 buffer.forEach(function (chunk) {
643 chunk.copy(body, i, 0, chunk.length)
646 if (self.encoding === null) {
649 response.body = body.toString(self.encoding)
651 } else if (buffer.length) {
652 response.body = buffer.join('')
657 response.body = JSON.parse(response.body)
661 self.emit('complete', response, response.body)
667 if (self.timeout && !self.timeoutTimer) {
668 self.timeoutTimer = setTimeout(function () {
670 var e = new Error("ETIMEDOUT")
672 self.emit("error", e)
675 // Set additional timeout on socket - in case if remote
676 // server freeze after sending headers
677 if (self.req.setTimeout) { // only works on node 0.6+
678 self.req.setTimeout(self.timeout, function () {
681 var e = new Error("ESOCKETTIMEDOUT")
682 e.code = "ESOCKETTIMEDOUT"
683 self.emit("error", e)
689 self.req.on('error', self.clientErrorHandler)
690 self.req.on('drain', function() {
693 self.on('end', function() {
694 if ( self.req.connection ) self.req.connection.removeListener('error', self._parserErrorHandler)
696 self.emit('request', self.req)
699 Request.prototype.abort = function () {
705 else if (this.response) {
706 this.response.abort()
712 Request.prototype.pipeDest = function (dest) {
713 var response = this.response
714 // Called after the response is received
716 dest.headers['content-type'] = response.headers['content-type']
717 if (response.headers['content-length']) {
718 dest.headers['content-length'] = response.headers['content-length']
721 if (dest.setHeader) {
722 for (var i in response.headers) {
723 dest.setHeader(i, response.headers[i])
725 dest.statusCode = response.statusCode
727 if (this.pipefilter) this.pipefilter(response, dest)
731 Request.prototype.setHeader = function (name, value, clobber) {
732 if (clobber === undefined) clobber = true
733 if (clobber || !this.headers.hasOwnProperty(name)) this.headers[name] = value
734 else this.headers[name] += ',' + value
737 Request.prototype.setHeaders = function (headers) {
738 for (var i in headers) {this.setHeader(i, headers[i])}
741 Request.prototype.qs = function (q, clobber) {
743 if (!clobber && this.uri.query) base = qs.parse(this.uri.query)
750 this.uri = url.parse(this.uri.href.split('?')[0] + '?' + qs.stringify(base))
755 Request.prototype.form = function (form) {
757 this.headers['content-type'] = 'application/x-www-form-urlencoded; charset=utf-8'
758 this.body = qs.stringify(form).toString('utf8')
761 // create form-data object
762 this._form = new FormData()
765 Request.prototype.multipart = function (multipart) {
769 if (!self.headers['content-type']) {
770 self.headers['content-type'] = 'multipart/related; boundary=' + self.boundary
772 self.headers['content-type'] = self.headers['content-type'].split(';')[0] + '; boundary=' + self.boundary
775 if (!multipart.forEach) throw new Error('Argument error, options.multipart.')
777 if (self.preambleCRLF) {
778 self.body.push(new Buffer('\r\n'))
781 multipart.forEach(function (part) {
783 if(body == null) throw Error('Body attribute missing in multipart.')
785 var preamble = '--' + self.boundary + '\r\n'
786 Object.keys(part).forEach(function (key) {
787 preamble += key + ': ' + part[key] + '\r\n'
790 self.body.push(new Buffer(preamble))
791 self.body.push(new Buffer(body))
792 self.body.push(new Buffer('\r\n'))
794 self.body.push(new Buffer('--' + self.boundary + '--'))
797 Request.prototype.json = function (val) {
798 this.setHeader('accept', 'application/json')
800 if (typeof val === 'boolean') {
801 if (typeof this.body === 'object') {
802 this.setHeader('content-type', 'application/json')
803 this.body = JSON.stringify(this.body)
806 this.setHeader('content-type', 'application/json')
807 this.body = JSON.stringify(val)
811 function getHeader(name, headers) {
812 var result, re, match
813 Object.keys(headers).forEach(function (key) {
814 re = new RegExp(name, 'i')
815 match = key.match(re)
816 if (match) result = headers[key]
820 Request.prototype.aws = function (opts, now) {
825 var date = new Date()
826 this.setHeader('date', date.toUTCString())
829 , secret: opts.secret
830 , verb: this.method.toUpperCase()
832 , contentType: getHeader('content-type', this.headers) || ''
833 , md5: getHeader('content-md5', this.headers) || ''
834 , amazonHeaders: aws.canonicalizeHeaders(this.headers)
836 if (opts.bucket && this.path) {
837 auth.resource = '/' + opts.bucket + this.path
838 } else if (opts.bucket && !this.path) {
839 auth.resource = '/' + opts.bucket
840 } else if (!opts.bucket && this.path) {
841 auth.resource = this.path
842 } else if (!opts.bucket && !this.path) {
845 auth.resource = aws.canonicalizeResource(auth.resource)
846 this.setHeader('authorization', aws.authorization(auth))
851 Request.prototype.oauth = function (_oauth) {
853 if (this.headers['content-type'] &&
854 this.headers['content-type'].slice(0, 'application/x-www-form-urlencoded'.length) ===
855 'application/x-www-form-urlencoded'
857 form = qs.parse(this.body)
859 if (this.uri.query) {
860 form = qs.parse(this.uri.query)
864 for (var i in form) oa[i] = form[i]
865 for (var i in _oauth) oa['oauth_'+i] = _oauth[i]
866 if (!oa.oauth_version) oa.oauth_version = '1.0'
867 if (!oa.oauth_timestamp) oa.oauth_timestamp = Math.floor( (new Date()).getTime() / 1000 ).toString()
868 if (!oa.oauth_nonce) oa.oauth_nonce = uuid().replace(/-/g, '')
870 oa.oauth_signature_method = 'HMAC-SHA1'
872 var consumer_secret = oa.oauth_consumer_secret
873 delete oa.oauth_consumer_secret
874 var token_secret = oa.oauth_token_secret
875 delete oa.oauth_token_secret
877 var baseurl = this.uri.protocol + '//' + this.uri.host + this.uri.pathname
878 var signature = oauth.hmacsign(this.method, baseurl, oa, consumer_secret, token_secret)
880 // oa.oauth_signature = signature
881 for (var i in form) {
882 if ( i.slice(0, 'oauth_') in _oauth) {
885 delete oa['oauth_'+i]
886 if (i !== 'x_auth_mode') delete oa[i]
889 this.headers.Authorization =
890 'OAuth '+Object.keys(oa).sort().map(function (i) {return i+'="'+oauth.rfc3986(oa[i])+'"'}).join(',')
891 this.headers.Authorization += ',oauth_signature="' + oauth.rfc3986(signature) + '"'
894 Request.prototype.jar = function (jar) {
897 if (this._redirectsFollowed === 0) {
898 this.originalCookieHeader = this.headers.cookie
904 this._disableCookies = true
906 // fetch cookie from the user defined cookie jar
907 cookies = jar.get({ url: this.uri.href })
909 // fetch cookie from the global cookie jar
910 cookies = cookieJar.get({ url: this.uri.href })
913 if (cookies && cookies.length) {
914 var cookieString = cookies.map(function (c) {
915 return c.name + "=" + c.value
918 if (this.originalCookieHeader) {
919 // Don't overwrite existing Cookie header
920 this.headers.cookie = this.originalCookieHeader + '; ' + cookieString
922 this.headers.cookie = cookieString
931 Request.prototype.pipe = function (dest, opts) {
933 if (this._destdata) {
934 throw new Error("You cannot pipe after data has been emitted from the response.")
935 } else if (this._ended) {
936 throw new Error("You cannot pipe after the response has been ended.")
938 stream.Stream.prototype.pipe.call(this, dest, opts)
943 this.dests.push(dest)
944 stream.Stream.prototype.pipe.call(this, dest, opts)
948 Request.prototype.write = function () {
949 if (!this._started) this.start()
950 return this.req.write.apply(this.req, arguments)
952 Request.prototype.end = function (chunk) {
953 if (chunk) this.write(chunk)
954 if (!this._started) this.start()
957 Request.prototype.pause = function () {
958 if (!this.response) this._paused = true
959 else this.response.pause.apply(this.response, arguments)
961 Request.prototype.resume = function () {
962 if (!this.response) this._paused = false
963 else this.response.resume.apply(this.response, arguments)
965 Request.prototype.destroy = function () {
966 if (!this._ended) this.end()
969 // organize params for post, put, head, del
970 function initParams(uri, options, callback) {
971 if ((typeof options === 'function') && !callback) callback = options
972 if (options && typeof options === 'object') {
974 } else if (typeof uri === 'string') {
980 return { uri: uri, options: options, callback: callback }
983 function request (uri, options, callback) {
984 if (typeof uri === 'undefined') throw new Error('undefined is not a valid uri or options object.')
985 if ((typeof options === 'function') && !callback) callback = options
986 if (options && typeof options === 'object') {
988 } else if (typeof uri === 'string') {
994 if (callback) options.callback = callback
995 var r = new Request(options)
999 module.exports = request
1001 request.initParams = initParams
1003 request.defaults = function (options, requester) {
1004 var def = function (method) {
1005 var d = function (uri, opts, callback) {
1006 var params = initParams(uri, opts, callback)
1007 for (var i in options) {
1008 if (params.options[i] === undefined) params.options[i] = options[i]
1010 if(typeof requester === 'function') {
1011 if(method === request) {
1014 params.options._requester = requester
1017 return method(params.options, params.callback)
1021 var de = def(request)
1022 de.get = def(request.get)
1023 de.post = def(request.post)
1024 de.put = def(request.put)
1025 de.head = def(request.head)
1026 de.del = def(request.del)
1027 de.cookie = def(request.cookie)
1028 de.jar = request.jar
1032 request.forever = function (agentOptions, optionsArg) {
1035 for (option in optionsArg) {
1036 options[option] = optionsArg[option]
1039 if (agentOptions) options.agentOptions = agentOptions
1040 options.forever = true
1041 return request.defaults(options)
1044 request.get = request
1045 request.post = function (uri, options, callback) {
1046 var params = initParams(uri, options, callback)
1047 params.options.method = 'POST'
1048 return request(params.uri || null, params.options, params.callback)
1050 request.put = function (uri, options, callback) {
1051 var params = initParams(uri, options, callback)
1052 params.options.method = 'PUT'
1053 return request(params.uri || null, params.options, params.callback)
1055 request.head = function (uri, options, callback) {
1056 var params = initParams(uri, options, callback)
1057 params.options.method = 'HEAD'
1058 if (params.options.body ||
1059 params.options.requestBodyStream ||
1060 (params.options.json && typeof params.options.json !== 'boolean') ||
1061 params.options.multipart) {
1062 throw new Error("HTTP HEAD requests MUST NOT include a request body.")
1064 return request(params.uri || null, params.options, params.callback)
1066 request.del = function (uri, options, callback) {
1067 var params = initParams(uri, options, callback)
1068 params.options.method = 'DELETE'
1069 if(typeof params.options._requester === 'function') {
1070 request = params.options._requester
1072 return request(params.uri || null, params.options, params.callback)
1074 request.jar = function () {
1075 return new CookieJar
1077 request.cookie = function (str) {
1078 if (str && str.uri) str = str.uri
1079 if (typeof str !== 'string') throw new Error("The cookie function only accepts STRING as param")
1080 return new Cookie(str)
1085 function getSafe (self, uuid) {
1086 if (typeof self === 'object' || typeof self === 'function') var safe = {}
1087 if (Array.isArray(self)) var safe = []
1091 Object.defineProperty(self, uuid, {})
1093 var attrs = Object.keys(self).filter(function (i) {
1094 if (i === uuid) return false
1095 if ( (typeof self[i] !== 'object' && typeof self[i] !== 'function') || self[i] === null) return true
1096 return !(Object.getOwnPropertyDescriptor(self[i], uuid))
1100 for (var i=0;i<attrs.length;i++) {
1101 if ( (typeof self[attrs[i]] !== 'object' && typeof self[attrs[i]] !== 'function') ||
1102 self[attrs[i]] === null
1104 safe[attrs[i]] = self[attrs[i]]
1106 recurse.push(attrs[i])
1107 Object.defineProperty(self[attrs[i]], uuid, {})
1111 for (var i=0;i<recurse.length;i++) {
1112 safe[recurse[i]] = getSafe(self[recurse[i]], uuid)
1118 function toJSON () {
1119 return getSafe(this, (((1+Math.random())*0x10000)|0).toString(16))
1122 Request.prototype.toJSON = toJSON