lib/request.js   B
last analyzed

Complexity

Total Complexity 46
Complexity/F 4.18

Size

Lines of Code 253
Function Count 11

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 0
wmc 46
c 1
b 0
f 0
nc 20480
mnd 3
bc 43
fnc 11
dl 0
loc 253
rs 8.3999
bpm 3.909
cpm 4.1818
noi 1

7 Functions

Rating   Name   Duplication   Size   Complexity  
A Request.qs 0 13 1
A request.js ➔ noop 0 1 1
A request.js ➔ Request 0 15 2
C Request.request 0 50 8
C Request.performRequest 0 75 10
A Request.convertError 0 9 3
C Request.handleFailure 0 37 8

How to fix   Complexity   

Complexity

Complex classes like lib/request.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
/* jshint -W100, -W071 */
2
var blocktrail = require('./blocktrail'),
3
    _ = require("lodash"),
4
    url = require('url'),
5
    qs = require('querystring'),
6
    q = require('q'),
7
    createHash = require('create-hash'),
8
    superagent = require('superagent'),
9
    superagentHttpSignature = require('superagent-http-signature/index-hmac-only');
10
11
var debug = require('debug')('blocktrail-sdk:request');
12
13
var isNodeJS = !process.browser;
14
15
var noop = function() {};
16
17
/**
18
 * Helper for doing HTTP requests
19
 *
20
 * @param options       object{
21
 *                          host: '',
22
 *                          endpoint: '', // base url for .request
23
 *                          auth: null || 'http-signature',
24
 *                          apiKey: 'API_KEY',
25
 *                          apiSecret: 'API_SECRET',
26
 *                          params: {}, // defaults
27
 *                          headers: {} // defaults
28
 *                      }
29
 * @constructor
30
 */
31
function Request(options) {
32
    var self = this;
33
34
    self.https = options.https;
35
    self.host = options.host;
36
    self.endpoint = options.endpoint;
37
    self.auth = options.auth;
38
    self.port = options.port;
39
    self.apiKey = options.apiKey;
40
    self.apiSecret = options.apiSecret;
41
    self.contentMd5 = typeof options.contentMd5 !== "undefined" ? options.contentMd5 : true;
42
43
    self.params = _.defaults({}, options.params);
44
    self.headers = _.defaults({}, options.headers);
45
}
46
47
/**
48
 * helper to make sure the query string is sorted in lexical order
49
 *
50
 * @param params
51
 * @returns {string}
52
 */
53
Request.qs = function(params) {
54
    var query = [];
55
    var qsKeys = Object.keys(params);
56
57
    qsKeys.sort();
58
    qsKeys.forEach(function(qsKey) {
59
        var qsChunk = {};
60
        qsChunk[qsKey] = params[qsKey];
61
        query.push(qs.stringify(qsChunk));
62
    });
63
64
    return query.join("&");
65
};
66
67
/**
68
 * execute request
69
 *
70
 * @param method        string      GET|POST|DELETE
71
 * @param resource      string      URL
72
 * @param params        object      are added to the querystring
73
 * @param data          object      is POSTed
74
 * @param fn
75
 * @returns q.Promise
76
 */
77
Request.prototype.request = function(method, resource, params, data, fn) {
78
    var self = this;
79
    self.deferred = q.defer();
80
81
    self.callback = fn || noop;
82
83
    var endpoint = url.parse(resource, true);
84
    var query = Request.qs(_.defaults({}, (params || {}), (endpoint.query || {}), (self.params || {})));
85
86
    self.path = ''.concat(self.endpoint, endpoint.pathname);
87
    if (query) {
88
        self.path = self.path.concat('?', query);
89
    }
90
91
    if (data) {
92
        self.payload = JSON.stringify(data);
93
        self.headers['Content-Type'] = 'application/json';
94
    } else {
95
        self.payload = "";
96
    }
97
98
    if (isNodeJS) {
99
        self.headers['Content-Length'] = self.payload ? self.payload.length : 0;
100
    }
101
102
    if (self.contentMd5 === true) {
103
        if (method === 'GET' || method === 'DELETE') {
104
            self.headers['Content-MD5'] = createHash('md5').update(self.path).digest().toString('hex');
105
        } else {
106
            self.headers['Content-MD5'] = createHash('md5').update(self.payload).digest().toString('hex');
107
        }
108
    }
109
110
    debug('%s %s %s', method, self.host, self.path);
111
112
    var opts = {
113
        hostname: self.host,
114
        path: self.path,
115
        port: self.port,
116
        method: method,
117
        headers: self.headers,
118
        auth: self.auth,
119
        agent: false,
120
        withCredentials: false
121
    };
122
123
    self.performRequest(opts);
124
125
    return self.deferred.promise;
126
};
127
128
Request.prototype.performRequest = function(options) {
129
    var self = this;
130
    var method = options.method;
131
    var signHMAC = false;
132
133
    if (options.auth === 'http-signature') {
134
        signHMAC = true;
135
        delete options.auth;
136
    }
137
138
    var uri = (self.https ? 'https://' : 'http://') + options.hostname + options.path;
139
140
    var request = superagent(method, uri);
141
142
    if (self.payload && (method === 'DELETE' || method === 'POST' || method === 'PUT' || method === 'PATCH')) {
143
        request.send(self.payload);
144
    }
145
146
    _.forEach(options.headers, function(value, header) {
147
        request.set(header, value);
148
    });
149
150
    if (signHMAC) {
151
        if (!self.apiSecret) {
152
            var error = new Error("Missing apiSecret! required to sign POST requests!");
153
            self.deferred.reject(error);
154
            return self.callback(error);
155
        }
156
157
        request.use(superagentHttpSignature({
158
            headers: ['(request-target)', 'content-md5'],
159
            algorithm: 'hmac-sha256',
160
            key: self.apiSecret,
161
            keyId: self.apiKey
162
        }));
163
    }
164
165
    request.end(function(error, res) {
166
        var body;
167
168
        if (error) {
169
            var err = Request.handleFailure(error.response && error.response.body, error.status);
170
171
            self.deferred.reject(err);
172
            return self.callback(err, error.response && error.response.body);
173
        }
174
175
        debug('response status code: %s content type: %s', res.status, res.headers['content-type']);
176
        if (!error && (res.headers['content-type'].indexOf('application/json') >= 0)) {
177
            try {
178
                body = JSON.parse(res.text);
179
            } catch (e) {
180
                error = e;
181
            }
182
        }
183
184
        if (!body) {
185
            body = res.text;
186
        }
187
188
        if (!error && res.status !== 200) {
189
            error = Request.handleFailure(res.text, res.statusCode);
190
        }
191
192
        if (error) {
193
            self.deferred.reject(error);
194
        } else {
195
            self.deferred.resolve(body);
196
        }
197
198
        return self.callback(error, body);
199
    });
200
201
    return self.deferred;
202
};
203
204
Request.handleFailure = function(body, statusCode) {
205
    var data, error;
206
    if (typeof body === "object") {
207
        data = body;
208
    } else {
209
        try {
210
            data = JSON.parse(body);
211
        } catch (e) {}
0 ignored issues
show
Coding Style Comprehensibility Best Practice introduced by
Empty catch clauses should be used with caution; consider adding a comment why this is needed.
Loading history...
212
    }
213
214
    if (data) {
215
        var msg = data.msg || "";
216
        if (!msg) {
217
            if (statusCode === 429) {
218
                msg = "Too Many Request";
219
            }
220
        }
221
222
        error = new Error(msg);
223
224
        Object.keys(data).forEach(function(k) {
225
            if (k !== "msg") {
226
                error[k] = data[k];
227
            }
228
        });
229
    } else if (body) {
230
        error = new Error(body);
231
    } else {
232
        error = new Error('Unknown Server Error');
233
    }
234
235
    if (statusCode) {
236
        error.statusCode = statusCode;
237
    }
238
239
    return Request.convertError(error);
240
};
241
242
Request.convertError = function(error) {
243
    if (error.requires_2fa) {
244
        return new blocktrail.WalletMissing2FAError();
245
    } else if (error.message.match(/Invalid two_factor_token/)) {
246
        return new blocktrail.WalletInvalid2FAError();
247
    }
248
249
    return error;
250
};
251
252
253
254
module.exports = Request;
255