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

box / box-typescript-sdk-gen / 14452166668

14 Apr 2025 05:43PM UTC coverage: 42.556%. First build
14452166668

Pull #585

github

web-flow
Merge e90c1fb25 into a7dcaf0f3
Pull Request #585: fix: Modify utils functions for browser (box/box-codegen#686)

4150 of 16757 branches covered (24.77%)

Branch coverage included in aggregate %.

26 of 37 new or added lines in 4 files covered. (70.27%)

15046 of 28351 relevant lines covered (53.07%)

139.77 hits per line

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

81.16
/src/networking/boxNetworkClient.ts
1
import nodeFetch, { RequestInit } from 'node-fetch';
225✔
2

3
import { BoxApiError, BoxSdkError } from '../box/errors';
225✔
4
import {
225✔
5
  ByteStream,
6
  FormData,
7
  generateByteStreamFromBuffer,
8
  isBrowser,
9
  readByteStream,
10
  calculateMD5Hash,
11
  multipartStreamToBuffer,
12
  multipartBufferToStream,
13
} from '../internal/utils';
14
import { sdkVersion } from './version';
225✔
15
import { NetworkClient } from './networkClient.generated';
16
import {
225✔
17
  SerializedData,
18
  jsonToSerializedData,
19
  sdIsMap,
20
  sdToJson,
21
  sdToUrlParams,
22
} from '../serialization/json';
23
import { Interceptor } from './interceptors.generated';
24
import { FetchOptions } from './fetchOptions.generated';
25
import { FetchResponse } from './fetchResponse.generated';
26
import { NetworkSession } from './network.generated';
225✔
27

28
export const userAgentHeader = `Box JavaScript generated SDK v${sdkVersion} (${
225✔
29
  isBrowser() ? navigator.userAgent : `Node ${process.version}`
225!
30
})`;
31

32
export const xBoxUaHeader = constructBoxUAHeader();
225✔
33
export const shouldIncludeBoxUaHeader = (options: FetchOptions) => {
225✔
34
  return !(
2,532✔
35
    isBrowser() &&
2,532!
36
    (options.responseFormat === 'binary' ||
37
      options.responseFormat === 'no_content')
38
  );
39
};
40

41
export interface MultipartItem {
42
  readonly partName: string;
43
  readonly data?: SerializedData;
44
  readonly fileStream?: ByteStream;
45
  readonly fileName?: string;
46
  readonly contentType?: string;
47
}
48

49
type FetchOptionsExtended = FetchOptions & {
50
  numRetries?: number;
51
};
52

53
async function createRequestInit(options: FetchOptions): Promise<RequestInit> {
54
  const {
55
    method = 'GET',
×
56
    headers = {},
21✔
57
    contentType: contentTypeInput = 'application/json',
×
58
    data,
59
    fileStream,
60
  } = options;
2,532✔
61

62
  const { contentHeaders = {}, body } = await (async (): Promise<{
2,532!
63
    contentHeaders: { [key: string]: string };
64
    body: ByteStream | string | Buffer;
65
  }> => {
2,532✔
66
    const contentHeaders: { [key: string]: string } = {};
2,532✔
67
    if (options.multipartData) {
2,532✔
68
      const FormDataClass = isBrowser() ? window.FormData : FormData;
180!
69
      const formData: any = new FormDataClass();
180✔
70
      for (const item of options.multipartData) {
180✔
71
        if (item.fileStream) {
357✔
72
          const buffer = await readByteStream(item.fileStream);
180✔
73
          const blob = isBrowser() ? new Blob([buffer]) : buffer;
180!
74
          contentHeaders['content-md5'] = await calculateMD5Hash(buffer);
180✔
75
          formData.append(item.partName, blob, {
180✔
76
            filename: item.fileName ?? 'file',
540✔
77
            contentType: item.contentType ?? 'application/octet-stream',
540✔
78
          });
79
        } else if (item.data) {
177!
80
          formData.append(item.partName, sdToJson(item.data));
177✔
81
        } else {
82
          throw new BoxSdkError({
×
83
            message: 'Multipart item must have either body or fileStream',
84
          });
85
        }
86
      }
87
      return {
180✔
88
        contentHeaders: {
89
          ...(!isBrowser() && {
360✔
90
            'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`,
91
          }),
92
          ...contentHeaders,
93
        },
94
        body: formData,
95
      };
96
    }
97

98
    contentHeaders['Content-Type'] = contentTypeInput;
2,352✔
99
    switch (contentTypeInput) {
2,352!
100
      case 'application/json':
101
      case 'application/json-patch+json':
102
        return { contentHeaders, body: sdToJson(data) };
1,869✔
103

104
      case 'application/x-www-form-urlencoded':
105
        return { contentHeaders, body: sdToUrlParams(data) };
456✔
106

107
      case 'application/octet-stream':
108
        if (!fileStream) {
27!
109
          throw new BoxSdkError({
×
110
            message:
111
              'fileStream required for application/octet-stream content type',
112
          });
113
        }
114
        return {
27✔
115
          contentHeaders,
116
          body: isBrowser()
27!
117
            ? await readByteStream(fileStream)
118
            : (fileStream as any),
119
        };
120

121
      default:
122
        throw new BoxSdkError({
×
123
          message: `Unsupported content type : ${contentTypeInput}`,
124
        });
125
    }
126
  })();
127

128
  return {
2,532✔
129
    method,
130
    headers: {
131
      // Only set content type if it is not a GET request
132
      ...(method != 'GET' && contentHeaders),
4,323✔
133
      ...headers,
134
      ...(options.auth && {
4,593✔
135
        Authorization: await options.auth.retrieveAuthorizationHeader(
136
          options.networkSession,
137
        ),
138
      }),
139
      ...(shouldIncludeBoxUaHeader(options) && {
5,064✔
140
        'User-Agent': userAgentHeader,
141
        'X-Box-UA': xBoxUaHeader,
142
      }),
143
      // Additional headers will override the default headers
144
      ...options.networkSession?.additionalHeaders,
7,596!
145
    },
146
    body: body as any,
147
    signal: options.cancellationToken as RequestInit['signal'],
148
    agent: options.networkSession?.agent,
7,596!
149
    ...(fileStream && isBrowser() && { duplex: 'half' }),
2,559!
150
  };
151
}
152

153
export class BoxNetworkClient implements NetworkClient {
225✔
154
  constructor(
155
    fields?: Omit<BoxNetworkClient, 'fetch'> &
156
      Partial<Pick<BoxNetworkClient, 'fetch'>>,
157
  ) {
158
    Object.assign(this, fields);
36,759✔
159
  }
160
  async fetch(options: FetchOptionsExtended): Promise<FetchResponse> {
161
    const numRetries = options.numRetries ?? 0;
2,532!
162
    const networkSession = options.networkSession ?? new NetworkSession({});
2,532!
163
    const fetchOptions: typeof options = networkSession.interceptors?.length
2,532!
164
      ? networkSession.interceptors.reduce(
165
          (modifiedOptions: FetchOptions, interceptor: Interceptor) =>
166
            interceptor.beforeRequest(modifiedOptions),
15✔
167
          options,
168
        )
169
      : options;
170
    const fileStreamBuffer = fetchOptions.fileStream
2,532✔
171
      ? await readByteStream(fetchOptions.fileStream)
172
      : void 0;
173
    const multipartBuffer = fetchOptions.multipartData
2,532✔
174
      ? await multipartStreamToBuffer(fetchOptions.multipartData)
175
      : void 0;
176

177
    const requestInit = await createRequestInit({
2,532✔
178
      ...fetchOptions,
179
      fileStream: fileStreamBuffer
2,532✔
180
        ? generateByteStreamFromBuffer(fileStreamBuffer)
181
        : void 0,
182
      multipartData: multipartBuffer
2,532✔
183
        ? multipartBufferToStream(multipartBuffer)
184
        : void 0,
185
    });
186

187
    const { params = {} } = fetchOptions;
2,532✔
188
    const response = await nodeFetch(
2,532✔
189
      ''.concat(
190
        fetchOptions.url,
191
        Object.keys(params).length === 0 || fetchOptions.url.endsWith('?')
5,259✔
192
          ? ''
193
          : '?',
194
        new URLSearchParams(params).toString(),
195
      ),
196
      { ...requestInit, redirect: isBrowser() ? 'follow' : 'manual' },
2,532!
197
    );
198

199
    const contentType = response.headers.get('content-type') ?? '';
2,529✔
200
    const ignoreResponseBody = fetchOptions.followRedirects === false;
2,529✔
201
    const responseBytesBuffer = !ignoreResponseBody
2,529✔
202
      ? await response.arrayBuffer()
203
      : new Uint8Array();
204

205
    const data = ((): SerializedData => {
2,529✔
206
      if (!ignoreResponseBody && contentType.includes('application/json')) {
2,529✔
207
        const text = new TextDecoder().decode(responseBytesBuffer);
1,942✔
208
        return jsonToSerializedData(text);
1,942✔
209
      }
210
      return void 0;
587✔
211
    })();
212

213
    const content = generateByteStreamFromBuffer(responseBytesBuffer);
2,529✔
214

215
    let fetchResponse: FetchResponse = {
2,529✔
216
      url: response.url,
217
      status: response.status,
218
      data,
219
      content,
220
      headers: Object.fromEntries(Array.from(response.headers.entries())),
221
    };
222
    if (networkSession.interceptors?.length) {
2,529!
223
      fetchResponse = networkSession.interceptors.reduce(
12✔
224
        (modifiedResponse: FetchResponse, interceptor: Interceptor) =>
225
          interceptor.afterRequest(modifiedResponse),
15✔
226
        fetchResponse,
227
      );
228
    }
229

230
    const shouldRetry = await networkSession.retryStrategy.shouldRetry(
2,529✔
231
      fetchOptions,
232
      fetchResponse,
233
      numRetries,
234
    );
235

236
    if (shouldRetry) {
2,526!
237
      const retryTimeout = networkSession.retryStrategy.retryAfter(
×
238
        fetchOptions,
239
        fetchResponse,
240
        numRetries,
241
      );
242
      await new Promise((resolve) => setTimeout(resolve, retryTimeout));
×
NEW
243
      return this.fetch({
×
244
        ...options,
245
        numRetries: numRetries + 1,
246
        fileStream: fileStreamBuffer
×
247
          ? generateByteStreamFromBuffer(fileStreamBuffer)
248
          : void 0,
249
        multipartData: multipartBuffer
×
250
          ? multipartBufferToStream(multipartBuffer)
251
          : void 0,
252
      });
253
    }
254

255
    if (
2,526✔
256
      fetchResponse.status >= 300 &&
2,746✔
257
      fetchResponse.status < 400 &&
258
      fetchOptions.followRedirects !== false
259
    ) {
260
      if (!fetchResponse.headers['location']) {
21✔
261
        throw new BoxSdkError({
3✔
262
          message: `Unable to follow redirect for ${fetchOptions.url}`,
263
        });
264
      }
265
      const sameOrigin =
266
        new URL(fetchResponse.headers['location']).origin ===
18✔
267
        new URL(fetchOptions.url).origin;
268
      return this.fetch({
15✔
269
        ...options,
270
        params: undefined,
271
        auth: sameOrigin ? fetchOptions.auth : undefined,
15!
272
        url: fetchResponse.headers['location'],
273
      });
274
    }
275

276
    if (fetchResponse.status >= 200 && fetchResponse.status < 400) {
2,505✔
277
      return fetchResponse;
2,339✔
278
    }
279

280
    const [code, contextInfo, requestId, helpUrl, message] = sdIsMap(
166✔
281
      fetchResponse.data,
282
    )
283
      ? [
284
          sdToJson(fetchResponse.data['code']),
285
          sdIsMap(fetchResponse.data['context_info'])
142✔
286
            ? fetchResponse.data['context_info']
287
            : undefined,
288
          sdToJson(fetchResponse.data['request_id']),
289
          sdToJson(fetchResponse.data['help_url']),
290
          sdToJson(fetchResponse.data['message']),
291
        ]
292
      : [];
293

294
    throw new BoxApiError({
166✔
295
      message: `${fetchResponse.status} ${message}; Request ID: ${requestId}`,
296
      timestamp: `${Date.now()}`,
297
      requestInfo: {
298
        method: requestInit.method!,
299
        url: fetchOptions.url,
300
        queryParams: params,
301
        headers: (requestInit.headers as { [key: string]: string }) ?? {},
498!
302
        body:
303
          typeof requestInit.body === 'string' ? requestInit.body : undefined,
166✔
304
      },
305
      responseInfo: {
306
        statusCode: fetchResponse.status,
307
        headers: fetchResponse.headers,
308
        body: fetchResponse.data,
309
        rawBody: new TextDecoder().decode(responseBytesBuffer),
310
        code: code,
311
        contextInfo: contextInfo,
312
        requestId: requestId,
313
        helpUrl: helpUrl,
314
      },
315
      dataSanitizer: networkSession.dataSanitizer,
316
    });
317
  }
318
}
319

320
function constructBoxUAHeader() {
321
  const analyticsIdentifiers = {
225✔
322
    agent: `box-javascript-generated-sdk/${sdkVersion}`,
323
    env: isBrowser()
225!
324
      ? navigator.userAgent
325
      : `Node/${process.version.replace('v', '')}`,
326
  } as Record<string, string>;
327

328
  return Object.keys(analyticsIdentifiers)
225✔
329
    .map((k) => `${k}=${analyticsIdentifiers[k]}`)
450✔
330
    .join('; ');
331
}
332

333
// Retry intervals are between 50% and 150% of the exponentially increasing base amount
334
const RETRY_RANDOMIZATION_FACTOR = 0.5;
225✔
335

336
/**
337
 * Calculate the exponential backoff time with randomized jitter
338
 * @param {int} numRetries Which retry number this one will be
339
 * @param {int} baseInterval The base retry interval set in config
340
 * @returns {int} The number of milliseconds after which to retry
341
 */
342
export function getRetryTimeout(numRetries: number, baseInterval: number) {
225✔
343
  var minRandomization = 1 - RETRY_RANDOMIZATION_FACTOR;
×
344
  var maxRandomization = 1 + RETRY_RANDOMIZATION_FACTOR;
×
345
  var randomization =
346
    Math.random() * (maxRandomization - minRandomization) + minRandomization;
×
347
  var exponential = Math.pow(2, numRetries - 1);
×
348
  return Math.ceil(exponential * baseInterval * randomization);
×
349
}
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