Skip to content

Commit 6d558a4

Browse files
Pedro Brancojoaopaulofonseca
Pedro Branco
authored andcommitted
Improve response handling
* Better error handling for both RPC and REST calls. * Handle `text/html` and `text/plain` differences from both request types correctly. * Proxy response errors as error messages for easier debugging. * Add a test for batched requests where one of the request fails. * Handle errors in binary-encoded requests. * Parse REST responses with JSONBigInt to avoid losing precision. This means that some of the response values will now be strings (e.g. the network difficulty on `getDifficulty()`).
1 parent d4ac43e commit 6d558a4

File tree

5 files changed

+73
-27
lines changed

5 files changed

+73
-27
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ This feature is available to all JSON-RPC methods that accept arguments.
115115

116116
### Floating point number precision in JavaScript
117117

118-
Due to [Javascript's limited floating point precision](http://floating-point-gui.de/), all big numbers (numbers with more than 15 significant digits) are returned as strings to prevent precision loss.
118+
Due to [JavaScript's limited floating point precision](http://floating-point-gui.de/), all big numbers (numbers with more than 15 significant digits) are returned as strings to prevent precision loss. This includes both the RPC and REST APIs.
119119

120120
### Version Checking
121121
By default, all methods are exposed on the client independently of the version it is connecting to. This is the most flexible option as defining methods for unavailable RPC calls does not cause any harm and the library is capable of handling a `Method not found` response error correctly.

src/index.js

+7-9
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,6 @@ class Client {
101101
this.request = Promise.promisifyAll(request.defaults({
102102
agentOptions: this.agentOptions,
103103
baseUrl: `${this.ssl.enabled ? 'https' : 'http'}://${this.host}:${this.port}`,
104-
json: true,
105104
strictSSL: this.ssl.strict,
106105
timeout: this.timeout
107106
}), { multiArgs: true });
@@ -139,7 +138,6 @@ class Client {
139138
return this.request.postAsync({
140139
auth: _.pickBy(this.auth, _.identity),
141140
body: JSON.stringify(body),
142-
json: false,
143141
uri: '/'
144142
}).bind(this)
145143
.then(this.parser.rpc);
@@ -158,7 +156,7 @@ class Client {
158156
encoding: extension === 'bin' ? null : undefined,
159157
url: `/rest/tx/${hash}.${extension}`
160158
}).bind(this)
161-
.then(this.parser.rest);
159+
.then(_.partial(this.parser.rest, extension));
162160
}).asCallback(callback);
163161
}
164162

@@ -176,7 +174,7 @@ class Client {
176174
encoding: extension === 'bin' ? null : undefined,
177175
url: `/rest/block${summary ? '/notxdetails/' : '/'}${hash}.${extension}`
178176
}).bind(this)
179-
.then(this.parser.rest);
177+
.then(_.partial(this.parser.rest, extension));
180178
}).asCallback(callback);
181179
}
182180

@@ -196,7 +194,7 @@ class Client {
196194
encoding: extension === 'bin' ? null : undefined,
197195
url: `/rest/headers/${count}/${hash}.${extension}`
198196
}).bind(this)
199-
.then(this.parser.rest);
197+
.then(_.partial(this.parser.rest, extension));
200198
}).asCallback(callback);
201199
}
202200

@@ -210,7 +208,7 @@ class Client {
210208

211209
return this.request.getAsync(`/rest/chaininfo.json`)
212210
.bind(this)
213-
.then(this.parser.rest)
211+
.then(_.partial(this.parser.rest, 'json'))
214212
.asCallback(callback);
215213
}
216214

@@ -231,7 +229,7 @@ class Client {
231229
encoding: extension === 'bin' ? null : undefined,
232230
url: `/rest/getutxos/checkmempool/${sets}.${extension}`
233231
}).bind(this)
234-
.then(this.parser.rest)
232+
.then(_.partial(this.parser.rest, extension))
235233
.asCallback(callback);
236234
}
237235

@@ -245,7 +243,7 @@ class Client {
245243

246244
return this.request.getAsync('/rest/mempool/contents.json')
247245
.bind(this)
248-
.then(this.parser.rest)
246+
.then(_.partial(this.parser.rest, 'json'))
249247
.asCallback(callback);
250248
}
251249

@@ -263,7 +261,7 @@ class Client {
263261

264262
return this.request.getAsync('/rest/mempool/info.json')
265263
.bind(this)
266-
.then(this.parser.rest)
264+
.then(_.partial(this.parser.rest, 'json'))
267265
.asCallback(callback);
268266
}
269267
}

src/parser.js

+25-14
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,18 @@ import _ from 'lodash';
1414
const { parse } = JSONBigInt({ storeAsString: true, strict: true }); // eslint-disable-line new-cap
1515

1616
/**
17-
* Get response result and errors.
17+
* Get RPC response body result.
1818
*/
1919

20-
function get(body, { headers = false, response } = {}) {
21-
if (!body) {
22-
throw new RpcError(response.statusCode, response.statusMessage);
23-
}
24-
20+
function getRpcResult(body, { headers = false, response } = {}) {
2521
if (body.error !== null) {
2622
throw new RpcError(
2723
_.get(body, 'error.code', -32603),
2824
_.get(body, 'error.message', 'An error occurred while processing the RPC call to bitcoind')
2925
);
3026
}
3127

28+
// Defensive measure. This should not happen on a RPC call.
3229
if (!_.has(body, 'result')) {
3330
throw new RpcError(-32700, 'Missing `result` on the RPC call result');
3431
}
@@ -54,22 +51,23 @@ export default class Parser {
5451
*/
5552

5653
rpc([response, body]) {
57-
// Body contains HTML (e.g. 401 Unauthorized).
58-
if (typeof body === 'string' && response.statusCode !== 200) {
59-
throw new RpcError(response.statusCode);
54+
// The RPC api returns a `text/html; charset=ISO-8859-1` encoded response with an empty string as the body
55+
// when an error occurs.
56+
if (typeof body === 'string' && response.headers['content-type'] !== 'application/json' && response.statusCode !== 200) {
57+
throw new RpcError(response.statusCode, response.statusMessage, { body });
6058
}
6159

6260
// Parsing the body with custom parser to support BigNumbers.
6361
body = parse(body);
6462

6563
if (!Array.isArray(body)) {
66-
return get(body, { headers: this.headers, response });
64+
return getRpcResult(body, { headers: this.headers, response });
6765
}
6866

6967
// Batch response parsing where each response may or may not be successful.
7068
const batch = body.map(response => {
7169
try {
72-
return get(response, { headers: false, response });
70+
return getRpcResult(response, { headers: false, response });
7371
} catch (e) {
7472
return e;
7573
}
@@ -82,9 +80,22 @@ export default class Parser {
8280
return batch;
8381
}
8482

85-
rest([response, body]) {
86-
if (body.error) {
87-
throw new RpcError(body.error.code, body.error.message);
83+
rest(extension, [response, body]) {
84+
// The REST api returns a `text/plain` encoded response with the error line and the control
85+
// characters \r\n. For readability and debuggability, the error message is set to this content.
86+
// When requesting a binary response, the body will be returned as a Buffer representation of
87+
// this error string.
88+
if (response.headers['content-type'] !== 'application/json' && response.statusCode !== 200) {
89+
if (body instanceof Buffer) {
90+
body = body.toString('utf-8');
91+
}
92+
93+
throw new RpcError(response.statusCode, body.replace('\r\n', ''), { body });
94+
}
95+
96+
// Parsing the body with custom parser to support BigNumbers.
97+
if (extension === 'json') {
98+
body = parse(body);
8899
}
89100

90101
if (this.headers) {

test/errors/rpc-error_test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
* Module dependencies.
44
*/
55

6-
import RpcError from '../../src/errors/rpc-error';
76
import { STATUS_CODES } from 'http';
7+
import RpcError from '../../src/errors/rpc-error';
88
import should from 'should';
99

1010
/**

test/index_test.js

+39-2
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@ describe('Client', () => {
105105
} catch (e) {
106106
e.should.be.an.instanceOf(RpcError);
107107
e.message.should.equal('Unauthorized');
108+
e.body.should.equal('');
108109
e.code.should.equal(401);
109-
e.status.should.equal(401);
110110
}
111111
});
112112

@@ -133,9 +133,20 @@ describe('Client', () => {
133133

134134
const [newAddress, addressValidation] = await new Client(config.bitcoind).command(batch);
135135

136-
addressValidation.should.have.properties('address', 'isvalid', 'ismine', 'scriptPubKey');
136+
addressValidation.should.have.properties('address', 'ismine', 'isvalid', 'scriptPubKey');
137137
newAddress.should.be.a.String();
138138
});
139+
140+
it('should return an error if one of the request fails', async () => {
141+
const batch = [{ method: 'foobar' }, { method: 'validateaddress', parameters: ['mkteeBFmGkraJaWN5WzqHCjmbQWVrPo5X3'] }];
142+
143+
const [newAddressError, addressValidation] = await new Client(config.bitcoind).command(batch);
144+
145+
addressValidation.should.have.properties('address', 'ismine', 'isvalid', 'scriptPubKey');
146+
newAddressError.should.be.an.instanceOf(RpcError);
147+
newAddressError.message.should.equal('Method not found');
148+
newAddressError.code.should.equal(-32601);
149+
});
139150
});
140151

141152
describe('headers', () => {
@@ -615,5 +626,31 @@ describe('Client', () => {
615626
information.usage.should.be.a.Number();
616627
});
617628
});
629+
630+
it('should throw an error if a method contains invalid arguments', async () => {
631+
try {
632+
await new Client(config.bitcoind).getTransactionByHash('foobar');
633+
634+
should.fail();
635+
} catch (e) {
636+
e.should.be.an.instanceOf(RpcError);
637+
e.body.should.equal('Invalid hash: foobar\r\n');
638+
e.message.should.equal('Invalid hash: foobar');
639+
e.code.should.equal(400);
640+
}
641+
});
642+
643+
it('should throw an error if a method in binary mode contains invalid arguments', async () => {
644+
try {
645+
await new Client(config.bitcoind).getTransactionByHash('foobar', { extension: 'bin' });
646+
647+
should.fail();
648+
} catch (e) {
649+
e.should.be.an.instanceOf(RpcError);
650+
e.body.should.equal('Invalid hash: foobar\r\n');
651+
e.message.should.equal('Invalid hash: foobar');
652+
e.code.should.equal(400);
653+
}
654+
});
618655
});
619656
});

0 commit comments

Comments
 (0)