box-node-sdk
Version:
Official SDK for Box Plaform APIs
284 lines (283 loc) • 12.6 kB
JavaScript
"use strict";
/**
* @fileoverview A Box API Request
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
// @NOTE(fschott) 08/05/2014: THIS FILE SHOULD NOT BE ACCESSED DIRECTLY OUTSIDE OF API-REQUEST-MANAGER
// This module is used by APIRequestManager to make requests. If you'd like to make requests to the
// Box API, consider using APIRequestManager instead. {@Link APIRequestManager}
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
var assert_1 = __importDefault(require("assert"));
var events_1 = require("events");
var http_status_1 = __importDefault(require("http-status"));
var config_1 = __importDefault(require("./util/config"));
var exponential_backoff_1 = __importDefault(require("./util/exponential-backoff"));
var request = require('@cypress/request');
// ------------------------------------------------------------------------------
// Private
// ------------------------------------------------------------------------------
// Message to replace removed headers with in the request
var REMOVED_HEADER_MESSAGE = '[REMOVED BY SDK]';
// Range of SERVER ERROR http status codes
var HTTP_STATUS_CODE_SERVER_ERROR_BLOCK_RANGE = [500, 599];
// Timer used to track elapsed time beginning from executing an async request to emitting the response.
var asyncRequestTimer;
// A map of HTTP status codes and whether or not they can be retried
var retryableStatusCodes = {};
retryableStatusCodes[http_status_1.default.REQUEST_TIMEOUT] = true;
retryableStatusCodes[http_status_1.default.TOO_MANY_REQUESTS] = true;
/**
* Returns true if the response info indicates a temporary/transient error.
*
* @param {?APIRequest~ResponseObject} response The response info from an API request,
* or undefined if the API request did not return any response info.
* @returns {boolean} True if the API call error is temporary (and hence can
* be retried). False otherwise.
* @private
*/
function isTemporaryError(response) {
var statusCode = response.statusCode;
// An API error is a temporary/transient if it returns a 5xx HTTP Status, with the exception of the 507 status.
// The API returns a 507 error when the user has run out of account space, in which case, it should be treated
// as a permanent, non-retryable error.
if (statusCode !== http_status_1.default.INSUFFICIENT_STORAGE &&
statusCode >= HTTP_STATUS_CODE_SERVER_ERROR_BLOCK_RANGE[0] &&
statusCode <= HTTP_STATUS_CODE_SERVER_ERROR_BLOCK_RANGE[1]) {
return true;
}
// An API error is a temporary/transient error if it returns a HTTP Status that indicates it is a temporary,
if (retryableStatusCodes[statusCode]) {
return true;
}
return false;
}
function isClientErrorResponse(response) {
if (!response || typeof response !== 'object') {
throw new Error("Expecting response to be an object, got: ".concat(String(response)));
}
var statusCode = response.statusCode;
if (typeof statusCode !== 'number') {
throw new Error("Expecting status code of response to be a number, got: ".concat(String(statusCode)));
}
return 400 <= statusCode && statusCode < 500;
}
function createErrorForResponse(response) {
var errorMessage = "".concat(response.statusCode, " - ").concat(http_status_1.default[response.statusCode]);
return new Error(errorMessage);
}
/**
* Determine whether a given request can be retried, based on its options
* @param {Object} options The request options
* @returns {boolean} Whether or not the request is retryable
* @private
*/
function isRequestRetryable(options) {
return !options.formData;
}
/**
* Clean sensitive headers from the request object. This prevents this data from
* propagating out to the SDK and getting unintentionally logged via the error or
* response objects. Note that this function modifies the given object and returns
* nothing.
*
* @param {APIRequest~RequestObject} requestObj Any request object
* @returns {void}
* @private
*/
function cleanSensitiveHeaders(requestObj) {
if (requestObj.headers) {
if (requestObj.headers.BoxApi) {
requestObj.headers.BoxApi = REMOVED_HEADER_MESSAGE;
}
if (requestObj.headers.Authorization) {
requestObj.headers.Authorization = REMOVED_HEADER_MESSAGE;
}
}
}
// ------------------------------------------------------------------------------
// Public
// ------------------------------------------------------------------------------
/**
* APIRequest helps to prepare and execute requests to the Box API. It supports
* retries, multipart uploads, and more.
*
* @param {Config} config Request-specific Config object
* @param {EventEmitter} eventBus Event bus for the SDK instance
* @constructor
*/
var APIRequest = /** @class */ (function () {
function APIRequest(config, eventBus) {
(0, assert_1.default)(config instanceof config_1.default, 'Config must be passed to APIRequest constructor');
(0, assert_1.default)(eventBus instanceof events_1.EventEmitter, 'Valid event bus must be passed to APIRequest constructor');
this.config = config;
this.eventBus = eventBus;
this.isRetryable = isRequestRetryable(config.request);
}
/**
* Executes the request with the given options. If a callback is provided, we'll
* handle the response via callbacks. Otherwise, the response will be streamed to
* via the stream property. You can access this stream with the getResponseStream()
* method.
*
* @param {APIRequest~Callback} [callback] Callback for handling the response
* @returns {void}
*/
APIRequest.prototype.execute = function (callback) {
var _this = this;
this._callback = callback || this._callback;
// Initiate an async- or stream-based request, based on the presence of the callback.
if (this._callback) {
// Start the request timer immediately before executing the async request
if (!asyncRequestTimer) {
asyncRequestTimer = process.hrtime();
}
this.request = request(this.config.request, this._handleResponse.bind(this));
}
else {
this.request = request(this.config.request);
this.stream = this.request;
this.stream.on('error', function (err) {
_this.eventBus.emit('response', err);
});
this.stream.on('response', function (response) {
if (isClientErrorResponse(response)) {
_this.eventBus.emit('response', createErrorForResponse(response));
return;
}
_this.eventBus.emit('response', null, response);
});
}
};
/**
* Return the response read stream for a request. This will be undefined until
* a stream-based request has been started.
*
* @returns {?ReadableStream} The response stream
*/
APIRequest.prototype.getResponseStream = function () {
return this.stream;
};
/**
* Handle the request response in the callback case.
*
* @param {?Error} err An error, if one occurred
* @param {Object} [response] The full response object, returned by the request module.
* Contains information about the request & response, including the response body itself.
* @returns {void}
* @private
*/
APIRequest.prototype._handleResponse = function (err /* FIXME */, response /* FIXME */) {
// Clean sensitive headers here to prevent the user from accidentily using/logging them in prod
cleanSensitiveHeaders(this.request);
// If the API connected successfully but responded with a temporary error (like a 5xx code,
// a rate limited response, etc.) then this is considered an error as well.
if (!err && isTemporaryError(response)) {
err = createErrorForResponse(response);
}
if (err) {
// Attach request & response information to the error object
err.request = this.request;
if (response) {
err.response = response;
err.statusCode = response.statusCode;
}
// Have the SDK emit the error response
this.eventBus.emit('response', err);
var isJWT = false;
if (this.config.request.hasOwnProperty('form') &&
this.config.request.form.hasOwnProperty('grant_type') &&
this.config.request.form.grant_type ===
'urn:ietf:params:oauth:grant-type:jwt-bearer') {
isJWT = true;
}
// If our APIRequest instance is retryable, attempt a retry. Otherwise, finish and propagate the error. Doesn't retry when the request is for JWT authentication, since that is handled in retryJWTGrant.
if (this.isRetryable && !isJWT) {
this._retry(err);
}
else {
this._finish(err);
}
return;
}
// If the request was successful, emit & propagate the response!
this.eventBus.emit('response', null, response);
this._finish(null, response);
};
/**
* Attempt a retry. If the request hasn't exceeded it's maximum number of retries,
* re-execute the request (after the retry interval). Otherwise, propagate a new error.
*
* @param {?Error} err An error, if one occurred
* @returns {void}
* @private
*/
APIRequest.prototype._retry = function (err /* FIXME */) {
this.numRetries = this.numRetries || 0;
if (this.numRetries < this.config.numMaxRetries) {
var retryTimeout;
this.numRetries += 1;
// If the retry strategy is defined, then use it to determine the time (in ms) until the next retry or to
// propagate an error to the user.
if (this.config.retryStrategy) {
// Get the total elapsed time so far since the request was executed
var totalElapsedTime = process.hrtime(asyncRequestTimer);
var totalElapsedTimeMS = totalElapsedTime[0] * 1000 + totalElapsedTime[1] / 1000000;
var retryOptions = {
error: err,
numRetryAttempts: this.numRetries,
numMaxRetries: this.config.numMaxRetries,
retryIntervalMS: this.config.retryIntervalMS,
totalElapsedTimeMS: totalElapsedTimeMS,
};
retryTimeout = this.config.retryStrategy(retryOptions);
// If the retry strategy doesn't return a number/time in ms, then propagate the response error to the user.
// However, if the retry strategy returns its own error, this will be propagated to the user instead.
if (typeof retryTimeout !== 'number') {
if (retryTimeout instanceof Error) {
err = retryTimeout;
}
this._finish(err);
return;
}
}
else if (err.hasOwnProperty('response') &&
err.response.hasOwnProperty('headers') &&
err.response.headers.hasOwnProperty('retry-after')) {
retryTimeout = err.response.headers['retry-after'] * 1000;
}
else {
retryTimeout = (0, exponential_backoff_1.default)(this.numRetries, this.config.retryIntervalMS);
}
setTimeout(this.execute.bind(this), retryTimeout);
}
else {
err.maxRetriesExceeded = true;
this._finish(err);
}
};
/**
* Propagate the response to the provided callback.
*
* @param {?Error} err An error, if one occurred
* @param {APIRequest~ResponseObject} response Information about the request & response
* @returns {void}
* @private
*/
APIRequest.prototype._finish = function (err, response) {
var callback = this._callback;
process.nextTick(function () {
if (err) {
callback(err);
return;
}
callback(null, response);
});
};
return APIRequest;
}());
module.exports = APIRequest;
//# sourceMappingURL=api-request.js.map