1 module.exports = regRequest
5 // 2. send authorization
6 // 3. content-type is 'application/json' -- metadata
8 var assert = require('assert')
9 var url = require('url')
10 var zlib = require('zlib')
11 var Stream = require('stream').Stream
12 var STATUS_CODES = require('http').STATUS_CODES
14 var request = require('request')
15 var once = require('once')
17 function regRequest (uri, params, cb_) {
18 assert(typeof uri === 'string', 'must pass uri to request')
19 assert(params && typeof params === 'object', 'must pass params to request')
20 assert(typeof cb_ === 'function', 'must pass callback to request')
22 params.method = params.method || 'GET'
23 this.log.verbose('request', 'uri', uri)
25 // Since there are multiple places where an error could occur,
26 // don't let the cb be called more than once.
29 if (uri.match(/^\/?favicon.ico/)) {
30 return cb(new Error("favicon.ico isn't a package, it's a picture."))
33 var adduserChange = /\/?-\/user\/org\.couchdb\.user:([^/]+)\/-rev/
34 var isUserChange = uri.match(adduserChange)
35 var adduserNew = /\/?-\/user\/org\.couchdb\.user:([^/?]+)$/
36 var isNewUser = uri.match(adduserNew)
37 var alwaysAuth = params.auth && params.auth.alwaysAuth
38 var isDelete = params.method === 'DELETE'
39 var isWrite = params.body || isDelete
41 if (isUserChange && !isWrite) {
42 return cb(new Error('trying to change user document without writing(?!)'))
45 // new users can *not* use auth, because they don't *have* auth yet
47 this.log.verbose('request', 'updating existing user; sending authorization')
49 } else if (isNewUser) {
50 this.log.verbose('request', "new user, so can't send auth")
52 } else if (alwaysAuth) {
53 this.log.verbose('request', 'always-auth set; sending authorization')
56 this.log.verbose('request', 'sending authorization for write operation')
59 // most of the time we don't want to auth
60 this.log.verbose('request', 'no auth needed')
65 this.attempt(function (operation) {
66 makeRequest.call(self, uri, params, function (er, parsed, raw, response) {
68 self.log.verbose('headers', response.headers)
69 if (response.headers['npm-notice']) {
70 self.log.warn('notice', response.headers['npm-notice'])
74 if (!er || (er.message && er.message.match(/^SSL Error/))) {
75 if (er) er.code = 'ESSL'
76 return cb(er, parsed, raw, response)
79 // Only retry on 408, 5xx or no `response`.
80 var statusCode = response && response.statusCode
82 var timeout = statusCode === 408
83 var serverError = statusCode >= 500
84 var statusRetry = !statusCode || timeout || serverError
85 if (er && statusRetry && operation.retry(er)) {
86 self.log.info('retry', 'will retry, error on last attempt: ' + er)
89 cb.apply(null, arguments)
94 function makeRequest (uri, params, cb_) {
96 var cb = once(function (er, parsed, raw, response) {
98 // The socket might be returned to a pool for re-use, so don’t keep
99 // the 'error' listener from here attached.
100 socket.removeListener('error', cb)
103 return cb_(er, parsed, raw, response)
106 var parsed = url.parse(uri)
109 // metadata should be compressed
110 headers['accept-encoding'] = 'gzip'
112 var er = this.authify(params.authed, parsed, headers, params.auth)
113 if (er) return cb_(er)
115 var opts = this.initialize(
122 opts.followRedirect = (typeof params.follow === 'boolean' ? params.follow : true)
123 opts.encoding = null // tell request let body be Buffer instance
126 this.log.verbose('etag', params.etag)
127 headers[params.method === 'GET' ? 'if-none-match' : 'if-match'] = params.etag
130 if (params.lastModified && params.method === 'GET') {
131 this.log.verbose('lastModified', params.lastModified)
132 headers['if-modified-since'] = params.lastModified
135 // figure out wth body is
137 if (Buffer.isBuffer(params.body)) {
138 opts.body = params.body
139 headers['content-type'] = 'application/json'
140 headers['content-length'] = params.body.length
141 } else if (typeof params.body === 'string') {
142 opts.body = params.body
143 headers['content-type'] = 'application/json'
144 headers['content-length'] = Buffer.byteLength(params.body)
145 } else if (params.body instanceof Stream) {
146 headers['content-type'] = 'application/octet-stream'
147 if (params.body.size) headers['content-length'] = params.body.size
149 delete params.body._etag
150 delete params.body._lastModified
151 opts.json = params.body
155 this.log.http('request', params.method, parsed.href || '/')
157 var done = requestDone.call(this, params.method, uri, cb)
158 var req = request(opts, params.streaming ? undefined : decodeResponseBody(done))
162 // This should not be necessary, as the HTTP implementation in Node
163 // passes errors occurring on the socket to the request itself. Being overly
164 // cautious comes at a low cost, though.
165 req.on('socket', function (s) {
167 socket.on('error', cb)
170 if (params.streaming) {
171 req.on('response', function (response) {
172 if (response.statusCode >= 400) {
174 response.on('data', function (data) {
177 response.on('end', function () {
178 decodeResponseBody(done)(null, response, Buffer.concat(parts))
181 response.on('end', function () {
182 // don't ever re-use connections that had server errors.
183 // those sockets connect to the Bad Place!
184 if (response.socket && response.statusCode > 500) {
185 response.socket.destroy()
189 return cb(null, response)
194 if (params.body && (params.body instanceof Stream)) {
195 params.body.pipe(req)
199 function decodeResponseBody (cb) {
200 return function (er, response, data) {
201 if (er) return cb(er, response, data)
203 // don't ever re-use connections that had server errors.
204 // those sockets connect to the Bad Place!
205 if (response.socket && response.statusCode > 500) {
206 response.socket.destroy()
209 if (response.headers['content-encoding'] !== 'gzip') {
210 return cb(er, response, data)
213 zlib.gunzip(data, function (er, buf) {
214 if (er) return cb(er, response, data)
216 cb(null, response, buf)
221 // cb(er, parsed, raw, response)
222 function requestDone (method, where, cb) {
223 return function (er, response, data) {
224 if (er) return cb(er)
226 var urlObj = url.parse(where)
227 if (urlObj.auth) urlObj.auth = '***'
228 this.log.http(response.statusCode, url.format(urlObj))
230 if (Buffer.isBuffer(data)) {
231 data = data.toString()
235 if (data && typeof data === 'string' && response.statusCode !== 304) {
237 parsed = JSON.parse(data)
239 ex.message += '\n' + data
240 this.log.verbose('bad json', data)
241 this.log.error('registry', 'error parsing json')
242 return cb(ex, null, data, response)
246 data = JSON.stringify(parsed)
249 // expect data with any error codes
250 if (!data && response.statusCode >= 400) {
251 var code = response.statusCode
253 makeError(code + ' ' + STATUS_CODES[code], null, code),
261 if (parsed && response.headers.etag) {
262 parsed._etag = response.headers.etag
265 if (parsed && response.headers['last-modified']) {
266 parsed._lastModified = response.headers['last-modified']
269 // for the search endpoint, the 'error' property can be an object
270 if (parsed && parsed.error && typeof parsed.error !== 'object' ||
271 response.statusCode >= 400) {
272 var w = url.parse(where).pathname.substr(1)
274 if (!w.match(/^-/)) {
276 name = decodeURIComponent(w[w.indexOf('_rewrite') + 1])
281 'Registry returned ' + response.statusCode +
287 } else if (name && parsed.error === 'not_found') {
288 er = makeError('404 Not Found: ' + name, name, response.statusCode)
291 parsed.error + ' ' + (parsed.reason || '') + ': ' + (name || w),
297 return cb(er, parsed, data, response)
301 function makeError (message, name, code) {
302 var er = new Error(message)
303 if (name) er.pkgid = name