• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

box / box-node-sdk / 15559984448

10 Jun 2025 12:51PM UTC coverage: 95.194%. Remained the same
15559984448

push

github

web-flow
chore: Bump dependencies and fix AI integration tests (#892)

846 of 916 branches covered (92.36%)

Branch coverage included in aggregate %.

2599 of 2653 new or added lines in 51 files covered. (97.96%)

1 existing line in 1 file now uncovered.

2977 of 3100 relevant lines covered (96.03%)

580.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.94
/src/api-request.ts
1
/**
2
 * @fileoverview A Box API Request
3
 */
4

5
// @NOTE(fschott) 08/05/2014: THIS FILE SHOULD NOT BE ACCESSED DIRECTLY OUTSIDE OF API-REQUEST-MANAGER
6
// This module is used by APIRequestManager to make requests. If you'd like to make requests to the
7
// Box API, consider using APIRequestManager instead. {@Link APIRequestManager}
8

9
// ------------------------------------------------------------------------------
10
// Requirements
11
// ------------------------------------------------------------------------------
12

13
import assert from 'assert';
108✔
14
import { EventEmitter } from 'events';
108✔
15
import httpStatusCodes from 'http-status';
108✔
16
import Config from './util/config';
108✔
17
import getRetryTimeout from './util/exponential-backoff';
108✔
18

19
const request = require('@cypress/request');
108✔
20

21
// ------------------------------------------------------------------------------
22
// Typedefs and Callbacks
23
// ------------------------------------------------------------------------------
24

25
// @NOTE(fschott) 08-19-2014: We cannot return the request/response objects directly because they contain loads of extra
26
// information, unnecessary bloat, circular dependencies, and cause an infinite loop when stringifying.
27
/**
28
 * The API response object includes information about the request made and its response. The information attached is a subset
29
 * of the information returned by the request module, which is too large and complex to be safely handled (contains circular
30
 * references, errors on serialization, etc.)
31
 *
32
 * @typedef {Object} APIRequest~ResponseObject
33
 * @property {APIRequest~RequestObject} request Information about the request that generated this response
34
 * @property {int} statusCode The response HTTP status code
35
 * @property {Object} headers A collection of response headers
36
 * @property {Object|Buffer|string} [body] The response body. Encoded to JSON by default, but can be a buffer
37
 *  (if encoding fails or if json encoding is disabled) or a string (if string encoding is enabled). Will be undefined
38
 *  if no response body is sent.
39
 */
40
type APIRequestResponseObject = {
41
  request: APIRequestRequestObject;
42
  statusCode: number;
43
  headers: Record<string, string>;
44
  body?: object | Buffer | string;
45
};
46

47
// @NOTE(fschott) 08-19-2014: We cannot return the request/response objects directly because they contain loads of extra
48
// information, unnecessary bloat, circular dependencies, and cause an infinite loop when stringifying.
49
/**
50
 * The API request object includes information about the request made. The information attached is a subset of the information
51
 * of a request module instance, which is too large and complex to be safely handled (contains circular references, errors on
52
 * serialization, etc.).
53
 *
54
 * @typedef {Object} APIRequest~RequestObject
55
 * @property {Object} uri Information about the request, including host, path, and the full 'href' url
56
 * @property {string} method The request method (GET, POST, etc.)
57
 * @property {Object} headers A collection of headers sent with the request
58
 */
59

60
type APIRequestRequestObject = {
61
  uri: Record<string, any>;
62
  method: string;
63
  headers: Record<string, string>;
64
};
65

66
/**
67
 * The error returned by APIRequest callbacks, which includes any relevent, available information about the request
68
 * and response. Note that these properties do not exist on stream errors, only errors retuned to the callback.
69
 *
70
 * @typedef {Error} APIRequest~Error
71
 * @property {APIRequest~RequestObject} request Information about the request that generated this error
72
 * @property {APIRequest~ResponseObject} [response] Information about the response related to this error, if available
73
 * @property {int} [statusCode] The response HTTP status code
74
 * @property {boolean} [maxRetriesExceeded] True iff the max number of retries were exceeded. Otherwise, undefined.
75
 */
76

77
type APIRequestError = {
78
  request: APIRequestRequestObject;
79
  response?: APIRequestResponseObject;
80
  statusCode?: number;
81
  maxRetriesExceeded?: boolean;
82
};
83

84
/**
85
 * Callback invoked when an APIRequest request is complete and finalized. On success,
86
 * propagates the relevent response information. An err will indicate an unresolvable issue
87
 * with the request (permanent failure or temp error response from the server, retried too many times).
88
 *
89
 * @callback APIRequest~Callback
90
 * @param {?APIRequest~Error} err If Error object, API request did not get back the data it was supposed to. This
91
 *  could be either because of a temporary error, or a more serious error connecting to the API.
92
 * @param {APIRequest~ResponseObject} response The response returned by an APIRequestManager request
93
 */
94
type APIRequestCallback = (
95
  err?: APIRequestError | null,
96
  response?: APIRequestResponseObject
97
) => void;
98

99
// ------------------------------------------------------------------------------
100
// Private
101
// ------------------------------------------------------------------------------
102

103
// Message to replace removed headers with in the request
104
var REMOVED_HEADER_MESSAGE = '[REMOVED BY SDK]';
108✔
105

106
// Range of SERVER ERROR http status codes
107
var HTTP_STATUS_CODE_SERVER_ERROR_BLOCK_RANGE = [500, 599];
108✔
108

109
// Timer used to track elapsed time beginning from executing an async request to emitting the response.
110
var asyncRequestTimer: [number, number];
111

112
// A map of HTTP status codes and whether or not they can be retried
113
var retryableStatusCodes: Record<number, boolean> = {};
108✔
114
retryableStatusCodes[httpStatusCodes.REQUEST_TIMEOUT] = true;
108✔
115
retryableStatusCodes[httpStatusCodes.TOO_MANY_REQUESTS] = true;
108✔
116

117
/**
118
 * Returns true if the response info indicates a temporary/transient error.
119
 *
120
 * @param {?APIRequest~ResponseObject} response The response info from an API request,
121
 * or undefined if the API request did not return any response info.
122
 * @returns {boolean} True if the API call error is temporary (and hence can
123
 * be retried). False otherwise.
124
 * @private
125
 */
126
function isTemporaryError(response: APIRequestResponseObject) {
127
  var statusCode = response.statusCode;
1,260✔
128

129
  // An API error is a temporary/transient if it returns a 5xx HTTP Status, with the exception of the 507 status.
130
  // The API returns a 507 error when the user has run out of account space, in which case, it should be treated
131
  // as a permanent, non-retryable error.
132
  if (
1,260✔
133
    statusCode !== httpStatusCodes.INSUFFICIENT_STORAGE &&
2,604✔
134
    statusCode >= HTTP_STATUS_CODE_SERVER_ERROR_BLOCK_RANGE[0] &&
135
    statusCode <= HTTP_STATUS_CODE_SERVER_ERROR_BLOCK_RANGE[1]
136
  ) {
137
    return true;
84✔
138
  }
139

140
  // An API error is a temporary/transient error if it returns a HTTP Status that indicates it is a temporary,
141
  if (retryableStatusCodes[statusCode]) {
1,176✔
142
    return true;
8✔
143
  }
144

145
  return false;
1,168✔
146
}
147

148
function isClientErrorResponse(response: { statusCode: number }) {
149
  if (!response || typeof response !== 'object') {
28!
NEW
150
    throw new Error(
×
151
      `Expecting response to be an object, got: ${String(response)}`
152
    );
153
  }
154
  const { statusCode } = response;
28✔
155
  if (typeof statusCode !== 'number') {
28!
NEW
156
    throw new Error(
×
157
      `Expecting status code of response to be a number, got: ${String(
158
        statusCode
159
      )}`
160
    );
161
  }
162
  return 400 <= statusCode && statusCode < 500;
28✔
163
}
164

165
function createErrorForResponse(response: { statusCode: number }): Error {
166
  var errorMessage = `${response.statusCode} - ${
96✔
167
    (httpStatusCodes as any)[response.statusCode]
168
  }`;
169
  return new Error(errorMessage);
96✔
170
}
171

172
/**
173
 * Determine whether a given request can be retried, based on its options
174
 * @param {Object} options The request options
175
 * @returns {boolean} Whether or not the request is retryable
176
 * @private
177
 */
178
function isRequestRetryable(options: Record<string, any>) {
179
  return !options.formData;
1,268✔
180
}
181

182
/**
183
 * Clean sensitive headers from the request object. This prevents this data from
184
 * propagating out to the SDK and getting unintentionally logged via the error or
185
 * response objects. Note that this function modifies the given object and returns
186
 * nothing.
187
 *
188
 * @param {APIRequest~RequestObject} requestObj Any request object
189
 * @returns {void}
190
 * @private
191
 */
192
function cleanSensitiveHeaders(requestObj: APIRequestRequestObject) {
193
  if (requestObj.headers) {
1,280✔
194
    if (requestObj.headers.BoxApi) {
1,204✔
195
      requestObj.headers.BoxApi = REMOVED_HEADER_MESSAGE;
20✔
196
    }
197
    if (requestObj.headers.Authorization) {
1,204✔
198
      requestObj.headers.Authorization = REMOVED_HEADER_MESSAGE;
1,164✔
199
    }
200
  }
201
}
202

203
// ------------------------------------------------------------------------------
204
// Public
205
// ------------------------------------------------------------------------------
206

207
/**
208
 * APIRequest helps to prepare and execute requests to the Box API. It supports
209
 * retries, multipart uploads, and more.
210
 *
211

212
 * @param {Config} config Request-specific Config object
213
 * @param {EventEmitter} eventBus Event bus for the SDK instance
214
 * @constructor
215
 */
216
class APIRequest {
108✔
217
  config: Config;
218
  eventBus: EventEmitter;
219
  isRetryable: boolean;
220

221
  _callback?: APIRequestCallback;
222
  request?: any; // request.Request;
223
  stream?: any; // request.Request;
224
  numRetries?: number;
225

226
  constructor(config: Config, eventBus: EventEmitter) {
227
    assert(
1,276✔
228
      config instanceof Config,
229
      'Config must be passed to APIRequest constructor'
230
    );
231
    assert(
1,272✔
232
      eventBus instanceof EventEmitter,
233
      'Valid event bus must be passed to APIRequest constructor'
234
    );
235
    this.config = config;
1,268✔
236
    this.eventBus = eventBus;
1,268✔
237
    this.isRetryable = isRequestRetryable(config.request);
1,268✔
238
  }
239

240
  /**
241
   * Executes the request with the given options. If a callback is provided, we'll
242
   * handle the response via callbacks. Otherwise, the response will be streamed to
243
   * via the stream property. You can access this stream with the getResponseStream()
244
   * method.
245
   *
246
   * @param {APIRequest~Callback} [callback] Callback for handling the response
247
   * @returns {void}
248
   */
249
  execute(callback?: APIRequestCallback) {
1,324✔
250
    this._callback = callback || this._callback;
1,324✔
251

252
    // Initiate an async- or stream-based request, based on the presence of the callback.
253
    if (this._callback) {
1,324✔
254
      // Start the request timer immediately before executing the async request
255
      if (!asyncRequestTimer) {
1,280✔
256
        asyncRequestTimer = process.hrtime();
72✔
257
      }
258
      this.request = request(
1,280✔
259
        this.config.request,
260
        this._handleResponse.bind(this)
261
      );
262
    } else {
263
      this.request = request(this.config.request);
44✔
264
      this.stream = this.request;
44✔
265
      this.stream.on('error', (err: any) => {
44✔
266
        this.eventBus.emit('response', err);
4✔
267
      });
268
      this.stream.on('response', (response: any) => {
44✔
269
        if (isClientErrorResponse(response)) {
28✔
270
          this.eventBus.emit('response', createErrorForResponse(response));
4✔
271
          return;
4✔
272
        }
273
        this.eventBus.emit('response', null, response);
24✔
274
      });
275
    }
276
  }
277

278
  /**
279
   * Return the response read stream for a request. This will be undefined until
280
   * a stream-based request has been started.
281
   *
282
   * @returns {?ReadableStream} The response stream
283
   */
284
  getResponseStream() {
108✔
285
    return this.stream;
28✔
286
  }
287

288
  /**
289
   * Handle the request response in the callback case.
290
   *
291
   * @param {?Error} err An error, if one occurred
292
   * @param {Object} [response] The full response object, returned by the request module.
293
   *  Contains information about the request & response, including the response body itself.
294
   * @returns {void}
295
   * @private
296
   */
297
  _handleResponse(err?: any /* FIXME */, response?: any /* FIXME */) {
108✔
298
    // Clean sensitive headers here to prevent the user from accidentily using/logging them in prod
299
    cleanSensitiveHeaders(this.request!);
1,280✔
300

301
    // If the API connected successfully but responded with a temporary error (like a 5xx code,
302
    // a rate limited response, etc.) then this is considered an error as well.
303
    if (!err && isTemporaryError(response)) {
1,280✔
304
      err = createErrorForResponse(response);
92✔
305
    }
306

307
    if (err) {
1,280✔
308
      // Attach request & response information to the error object
309
      err.request = this.request;
112✔
310
      if (response) {
112✔
311
        err.response = response;
96✔
312
        err.statusCode = response.statusCode;
96✔
313
      }
314

315
      // Have the SDK emit the error response
316
      this.eventBus.emit('response', err);
112✔
317

318
      var isJWT = false;
112✔
319
      if (
112!
320
        this.config.request.hasOwnProperty('form') &&
112!
321
        this.config.request.form.hasOwnProperty('grant_type') &&
322
        this.config.request.form.grant_type ===
323
          'urn:ietf:params:oauth:grant-type:jwt-bearer'
324
      ) {
NEW
325
        isJWT = true;
×
326
      }
327
      // 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.
328
      if (this.isRetryable && !isJWT) {
112✔
329
        this._retry(err);
84✔
330
      } else {
331
        this._finish(err);
28✔
332
      }
333

334
      return;
112✔
335
    }
336

337
    // If the request was successful, emit & propagate the response!
338
    this.eventBus.emit('response', null, response);
1,168✔
339
    this._finish(null, response);
1,168✔
340
  }
341

342
  /**
343
   * Attempt a retry. If the request hasn't exceeded it's maximum number of retries,
344
   * re-execute the request (after the retry interval). Otherwise, propagate a new error.
345
   *
346
   * @param {?Error} err An error, if one occurred
347
   * @returns {void}
348
   * @private
349
   */
350
  _retry(err?: any /* FIXME */) {
108✔
351
    this.numRetries = this.numRetries || 0;
84✔
352

353
    if (this.numRetries < this.config.numMaxRetries) {
84✔
354
      var retryTimeout;
355
      this.numRetries += 1;
72✔
356
      // If the retry strategy is defined, then use it to determine the time (in ms) until the next retry or to
357
      // propagate an error to the user.
358
      if (this.config.retryStrategy) {
72✔
359
        // Get the total elapsed time so far since the request was executed
360
        var totalElapsedTime = process.hrtime(asyncRequestTimer);
32✔
361
        var totalElapsedTimeMS =
362
          totalElapsedTime[0] * 1000 + totalElapsedTime[1] / 1000000;
32✔
363
        var retryOptions = {
32✔
364
          error: err,
365
          numRetryAttempts: this.numRetries,
366
          numMaxRetries: this.config.numMaxRetries,
367
          retryIntervalMS: this.config.retryIntervalMS,
368
          totalElapsedTimeMS,
369
        };
370

371
        retryTimeout = this.config.retryStrategy(retryOptions);
32✔
372

373
        // If the retry strategy doesn't return a number/time in ms, then propagate the response error to the user.
374
        // However, if the retry strategy returns its own error, this will be propagated to the user instead.
375
        if (typeof retryTimeout !== 'number') {
32✔
376
          if (retryTimeout instanceof Error) {
8✔
377
            err = retryTimeout;
4✔
378
          }
379
          this._finish(err);
8✔
380
          return;
8✔
381
        }
382
      } else if (
40!
383
        err.hasOwnProperty('response') &&
86✔
384
        err.response.hasOwnProperty('headers') &&
385
        err.response.headers.hasOwnProperty('retry-after')
386
      ) {
NEW
387
        retryTimeout = err.response.headers['retry-after'] * 1000;
×
388
      } else {
389
        retryTimeout = getRetryTimeout(
40✔
390
          this.numRetries,
391
          this.config.retryIntervalMS
392
        );
393
      }
394
      setTimeout(this.execute.bind(this), retryTimeout);
64✔
395
    } else {
396
      err.maxRetriesExceeded = true;
12✔
397
      this._finish(err);
12✔
398
    }
399
  }
400

401
  /**
402
   * Propagate the response to the provided callback.
403
   *
404
   * @param {?Error} err An error, if one occurred
405
   * @param {APIRequest~ResponseObject} response Information about the request & response
406
   * @returns {void}
407
   * @private
408
   */
409
  _finish(err?: any, response?: APIRequestResponseObject) {
108✔
410
    var callback = this._callback!;
1,216✔
411
    process.nextTick(() => {
1,216✔
412
      if (err) {
1,216✔
413
        callback(err);
48✔
414
        return;
48✔
415
      }
416

417
      callback(null, response);
1,168✔
418
    });
419
  }
420
}
108✔
421

422
/**
423
 * @module box-node-sdk/lib/api-request
424
 * @see {@Link APIRequest}
425
 */
426
export = APIRequest;
108✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc