diff --git a/src/call_builder.js b/src/call_builder.js index a7b112c9a..f97924774 100644 --- a/src/call_builder.js +++ b/src/call_builder.js @@ -1,5 +1,6 @@ import {NotFoundError, NetworkError, BadRequestError} from "./errors"; import forEach from 'lodash/forEach'; +import { Config } from "./config"; let URI = require("urijs"); let URITemplate = require("urijs/src/URITemplate"); @@ -17,6 +18,7 @@ let toBluebird = require("bluebird").resolve; */ export class CallBuilder { constructor(serverUrl) { + this.url = serverUrl; this.filter = []; this.originalSegments = this.url.segment() || []; diff --git a/src/config.js b/src/config.js index b0a0947a4..9ddcd51cc 100644 --- a/src/config.js +++ b/src/config.js @@ -1,7 +1,8 @@ import clone from 'lodash/clone'; let defaultConfig = { - allowHttp: false + allowHttp: false, + timeout: 0 }; let config = clone(defaultConfig); @@ -13,11 +14,13 @@ let config = clone(defaultConfig); * ``` * import {Config} from 'stellar-sdk'; * Config.setAllowHttp(true); + * Config.setTimout(5000); * ``` * * Usage browser: * ``` * StellarSdk.Config.setAllowHttp(true); + * StellarSdk.Config.setTimout(5000); * ``` * @static */ @@ -32,6 +35,16 @@ class Config { config.allowHttp = value; } + /** + * Sets `timeout` flag globally. When set to anything besides 0, the request will timeout after specified time (ms). + * Default: 0. + * @param {number} value + * @static + */ + static setTimeout(value) { + config.timeout = value; + } + /** * Returns the value of `allowHttp` flag. * @static @@ -40,6 +53,14 @@ class Config { return clone(config.allowHttp); } + /** + * Returns the value of `timeout` flag. + * @static + */ + static getTimeout() { + return clone(config.timeout); + } + /** * Sets all global config flags to default values. * @static diff --git a/src/federation_server.js b/src/federation_server.js index 386439899..7717fcf29 100644 --- a/src/federation_server.js +++ b/src/federation_server.js @@ -71,6 +71,7 @@ export class FederationServer { * @param {string} value Stellar Address (ex. `bob*stellar.org`) * @param {object} [opts] * @param {boolean} [opts.allowHttp] - Allow connecting to http servers, default: `false`. This must be set to false in production deployments! + * @param {number} [opts.timeout] - Allow a timeout, default: 0. Allows user to avoid nasty lag due to TOML resolve issue. * @returns {Promise} */ static resolve(value, opts = {}) { @@ -109,10 +110,11 @@ export class FederationServer { * @param {string} domain Domain to get federation server for * @param {object} [opts] * @param {boolean} [opts.allowHttp] - Allow connecting to http servers, default: `false`. This must be set to false in production deployments! + * @param {number} [opts.timeout] - Allow a timeout, default: 0. Allows user to avoid nasty lag due to TOML resolve issue. * @returns {Promise} */ static createForDomain(domain, opts = {}) { - return StellarTomlResolver.resolve(domain) + return StellarTomlResolver.resolve(domain, opts) .then(tomlObject => { if (!tomlObject.FEDERATION_SERVER) { return Promise.reject(new Error('stellar.toml does not contain FEDERATION_SERVER field')); @@ -161,7 +163,9 @@ export class FederationServer { } _sendRequest(url) { - return axios.get(url.toString(), {maxContentLength: FEDERATION_RESPONSE_MAX_SIZE}) + let timeout = this.timeout | Config.getTimeout(); + + return axios.get(url.toString(), {maxContentLength: FEDERATION_RESPONSE_MAX_SIZE, timeout}) .then(response => { if (typeof response.data.memo != "undefined" && typeof response.data.memo != 'string') { throw new Error("memo value should be of type string"); diff --git a/src/stellar_toml_resolver.js b/src/stellar_toml_resolver.js index 917ea6d27..a49b7adff 100644 --- a/src/stellar_toml_resolver.js +++ b/src/stellar_toml_resolver.js @@ -26,19 +26,27 @@ export class StellarTomlResolver { * @param {string} domain Domain to get stellar.toml file for * @param {object} [opts] * @param {boolean} [opts.allowHttp] - Allow connecting to http servers, default: `false`. This must be set to false in production deployments! + * @param {number} [opts.timeout] - Allow a timeout, default: 0. Allows user to avoid nasty lag due to TOML resolve issue. * @returns {Promise} */ static resolve(domain, opts = {}) { let allowHttp = Config.isAllowHttp(); + let timeout = Config.getTimeout(); + if (typeof opts.allowHttp !== 'undefined') { allowHttp = opts.allowHttp; } + if (typeof opts.timeout === 'number') { + timeout = opts.timeout; + } + let protocol = 'https'; if (allowHttp) { protocol = 'http'; } - return axios.get(`${protocol}://${domain}/.well-known/stellar.toml`, {maxContentLength: STELLAR_TOML_MAX_SIZE}) + + return axios.get(`${protocol}://${domain}/.well-known/stellar.toml`, {maxContentLength: STELLAR_TOML_MAX_SIZE, timeout}) .then(response => { try { let tomlObject = toml.parse(response.data); diff --git a/test/unit/federation_server_test.js b/test/unit/federation_server_test.js index f3d8f4583..4b454a2f7 100644 --- a/test/unit/federation_server_test.js +++ b/test/unit/federation_server_test.js @@ -227,5 +227,101 @@ FEDERATION_SERVER="https://api.stellar.org/federation" .then(() => tempServer.close()); }); }); + + }); + + describe('FederationServer timeout', function () { + it("times out when resolveAddress response lags", function (done) { + StellarSdk.Config.setTimeout(100); + + // Unable to create temp server in a browser + if (typeof window != 'undefined') { + return done(); + } + + let tempServer = http.createServer((req, res) => { + res.setHeader('Content-Type', 'application/json; charset=UTF-8'); + setTimeout(() => {}, 10000); + }).listen(4444, () => { + new StellarSdk.FederationServer('http://localhost:4444/federation', 'stellar.org', {allowHttp: true}) + .resolveAddress('bob*stellar.org') + .should.be.rejectedWith(/timeout of 100ms exceeded/) + .notify(done) + .then(() => tempServer.close()); + }); + }); + + it("times out when resolveAccountId response lags", function (done) { + StellarSdk.Config.setTimeout(100); + + // Unable to create temp server in a browser + if (typeof window != 'undefined') { + return done(); + } + + let tempServer = http.createServer((req, res) => { + res.setHeader('Content-Type', 'application/json; charset=UTF-8'); + setTimeout(() => {}, 10000); + }).listen(4444, () => { + new StellarSdk.FederationServer('http://localhost:4444/federation', 'stellar.org', {allowHttp: true}) + .resolveAccountId('GB5XVAABEQMY63WTHDQ5RXADGYF345VWMNPTN2GFUDZT57D57ZQTJ7PS') + .should.be.rejectedWith(/timeout of 100ms exceeded/) + .notify(done) + .then(() => tempServer.close()); + }); + }); + + it("times out when resolveTransactionId response lags", function (done) { + StellarSdk.Config.setTimeout(100); + + // Unable to create temp server in a browser + if (typeof window != 'undefined') { + return done(); + } + + let tempServer = http.createServer((req, res) => { + res.setHeader('Content-Type', 'application/json; charset=UTF-8'); + setTimeout(() => {}, 10000); + }).listen(4444, () => { + new StellarSdk.FederationServer('http://localhost:4444/federation', 'stellar.org', {allowHttp: true}) + .resolveTransactionId('3389e9f0f1a65f19736cacf544c2e825313e8447f569233bb8db39aa607c8889') + .should.be.rejectedWith(/timeout of 100ms exceeded/) + .notify(done) + .then(() => tempServer.close()); + }); + }); + + it("times out when createForDomain response lags", function (done) { + // Unable to create temp server in a browser + if (typeof window != 'undefined') { + return done(); + } + + let tempServer = http.createServer((req, res) => { + setTimeout(() => {}, 1000); + }).listen(4444, () => { + StellarSdk.FederationServer.createForDomain("localhost:4444", {allowHttp: true, timeout: 100}) + .should.be.rejectedWith(/timeout of 100ms exceeded/) + .notify(done) + .then(() => tempServer.close()); + }); + }); + + it("times out when resolve response lags", function (done) { + // Unable to create temp server in a browser + if (typeof window != 'undefined') { + return done(); + } + + let tempServer = http.createServer((req, res) => { + setTimeout(() => {}, 10000); + }).listen(4444, () => { + StellarSdk.FederationServer + .resolve('bob*localhost:4444', {allowHttp: true, timeout: 100}) + .should.eventually.be.rejectedWith(/timeout of 100ms exceeded/) + .notify(done) + .then(() => tempServer.close()); + }); + }); }); }); diff --git a/test/unit/stellar_toml_resolver_test.js b/test/unit/stellar_toml_resolver_test.js index 88e97048a..cfa3b82bf 100644 --- a/test/unit/stellar_toml_resolver_test.js +++ b/test/unit/stellar_toml_resolver_test.js @@ -106,5 +106,39 @@ FEDERATION_SERVER="https://api.stellar.org/federation" .then(() => tempServer.close()); }); }); + + it("rejects after given timeout when global Config.timeout flag is set", function (done) { + StellarSdk.Config.setTimeout(100); + + // Unable to create temp server in a browser + if (typeof window != 'undefined') { + return done(); + } + + let tempServer = http.createServer((req, res) => { + setTimeout(() => {}, 10000); + }).listen(4444, () => { + StellarSdk.StellarTomlResolver.resolve("localhost:4444", {allowHttp: true}) + .should.be.rejectedWith(/timeout of 100ms exceeded/) + .notify(done) + .then(() => tempServer.close()); + }); + }); + + it("rejects after given timeout when timeout specified in StellarTomlResolver opts param", function (done) { + // Unable to create temp server in a browser + if (typeof window != 'undefined') { + return done(); + } + + let tempServer = http.createServer((req, res) => { + setTimeout(() => {}, 10000); + }).listen(4444, () => { + StellarSdk.StellarTomlResolver.resolve("localhost:4444", {allowHttp: true, timeout: 100}) + .should.be.rejectedWith(/timeout of 100ms exceeded/) + .notify(done) + .then(() => tempServer.close()); + }); + }); }); });