Skip to content

Commit 797ca1b

Browse files
connection: refetch token if invalid.
1 parent 4e6f384 commit 797ca1b

File tree

2 files changed

+77
-16
lines changed

2 files changed

+77
-16
lines changed

lib/common/connection.js

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
'use strict';
2323

2424
var events = require('events');
25+
var extend = require('extend');
2526
var fs = require('fs');
2627
var GAPIToken = require('gapitoken');
2728
var nodeutil = require('util');
@@ -35,6 +36,9 @@ var METADATA_TOKEN_URL =
3536
'http://metadata/computeMetadata/v1/instance/service-accounts/default/' +
3637
'token';
3738

39+
/** @const {number} Maximum amount of times to attempt refreshing a token. */
40+
var MAX_TOKEN_REFRESH_ATTEMPTS = 1;
41+
3842
/** @const {object} gcloud-node's package.json file. */
3943
var PKG = require('../../package.json');
4044

@@ -217,7 +221,9 @@ Connection.prototype.fetchServiceAccountToken_ = function(callback) {
217221

218222
/**
219223
* Make an authorized request if the current connection token is still valid. If
220-
* it's not, try to reconnect.
224+
* it's not, try to reconnect to the limit specified by
225+
* MAX_TOKEN_REFRESH_ATTEMPTS. If a valid connection still cannot be made,
226+
* execute the callback with the API error.
221227
*
222228
* @param {object} requestOptions - Request options.
223229
* @param {function=} callback - The callback function.
@@ -227,14 +233,25 @@ Connection.prototype.fetchServiceAccountToken_ = function(callback) {
227233
*/
228234
Connection.prototype.req = function(requestOptions, callback) {
229235
var that = this;
236+
var tokenRefreshAttempts = 0;
230237
callback = callback || util.noop;
231-
this.createAuthorizedReq(requestOptions, function(err, authorizedReq) {
238+
function onAuthorizedReq(err, authorizedReq) {
232239
if (err) {
233240
callback(err);
234241
return;
235242
}
236-
that.requester(authorizedReq, callback);
237-
});
243+
that.requester(authorizedReq, function(err) {
244+
if (err && err.code === 401 &&
245+
++tokenRefreshAttempts <= MAX_TOKEN_REFRESH_ATTEMPTS) {
246+
// Invalid token. Try to fetch a new one.
247+
that.token = null;
248+
that.createAuthorizedReq(requestOptions, onAuthorizedReq);
249+
return;
250+
}
251+
callback.apply(null, util.toArray(arguments));
252+
});
253+
}
254+
this.createAuthorizedReq(requestOptions, onAuthorizedReq);
238255
};
239256

240257
/**
@@ -246,10 +263,10 @@ Connection.prototype.req = function(requestOptions, callback) {
246263
* @example
247264
* conn.createAuthorizedReq({}, function(err) {});
248265
*/
249-
Connection.prototype.createAuthorizedReq = function(reqOpts, callback) {
266+
Connection.prototype.createAuthorizedReq = function(requestOptions, callback) {
250267
var that = this;
251-
// Add user agent.
252-
reqOpts.headers = reqOpts.headers || {};
268+
269+
var reqOpts = extend(true, {}, requestOptions, { headers: {} });
253270

254271
if (reqOpts.headers['User-Agent']) {
255272
reqOpts.headers['User-Agent'] += '; ' + USER_AGENT;
@@ -305,9 +322,11 @@ Connection.prototype.isConnected = function() {
305322
* @return {object} Authorized request options.
306323
*/
307324
Connection.prototype.authorizeReq = function(requestOptions) {
308-
requestOptions.headers = requestOptions.headers || {};
309-
requestOptions.headers.Authorization = 'Bearer ' + this.token.accessToken;
310-
return requestOptions;
325+
return extend(true, {}, requestOptions, {
326+
headers: {
327+
Authorization: 'Bearer ' + this.token.accessToken
328+
}
329+
});
311330
};
312331

313332
/**

test/common/connection.js

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var async = require('async');
2323
var path = require('path');
2424

2525
var connection = require('../../lib/common/connection.js');
26+
var util = require('../../lib/common/util.js');
2627

2728
describe('Connection', function() {
2829
var conn;
@@ -141,13 +142,54 @@ describe('Connection', function() {
141142
conn.req({ uri: 'https://someuri' }, function() {});
142143
});
143144

144-
it('should pass error to callback', function(done) {
145-
var error = new Error('Something terrible happened.');
146-
conn.fetchToken = function(cb) {
147-
cb(error);
145+
it('should fetch a new token if API returns a 401', function() {
146+
var fetchTokenCount = 0;
147+
conn.fetchToken = function(callback) {
148+
fetchTokenCount++;
149+
callback(null, tokenNeverExpires);
150+
};
151+
conn.requester = function(req, callback) {
152+
if (fetchTokenCount === 1) {
153+
callback({ code: 401 });
154+
} else {
155+
callback(null);
156+
}
157+
};
158+
conn.req({ uri: 'https://someuri' }, function() {});
159+
assert.equal(fetchTokenCount, 2);
160+
});
161+
162+
it('should try API request 2 times', function(done) {
163+
// Fail 1: invalid token.
164+
// -- try to get token --
165+
// Fail 2: invalid token.
166+
// -- execute callback with error.
167+
var error = { code: 401 };
168+
var requesterCount = 0;
169+
conn.fetchToken = function(callback) {
170+
callback(null, tokenNeverExpires);
171+
};
172+
conn.requester = function(req, callback) {
173+
requesterCount++;
174+
callback(error);
175+
};
176+
conn.req({ uri: 'https://someuri' }, function(err) {
177+
assert.equal(requesterCount, 2);
178+
assert.deepEqual(err, error);
179+
done();
180+
});
181+
});
182+
183+
it('should pass all arguments from requester to callback', function(done) {
184+
var args = [null, 1, 2, 3];
185+
conn.fetchToken = function(callback) {
186+
callback(null, tokenNeverExpires);
187+
};
188+
conn.requester = function(req, callback) {
189+
callback.apply(null, args);
148190
};
149-
conn.req({}, function(err) {
150-
assert.equal(error, err);
191+
conn.req({ uri: 'https://someuri' }, function() {
192+
assert.deepEqual(util.toArray(arguments), args);
151193
done();
152194
});
153195
});

0 commit comments

Comments
 (0)