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

box / box-typescript-sdk-gen / 13306896857

13 Feb 2025 11:41AM UTC coverage: 42.608% (-0.04%) from 42.649%
13306896857

Pull #516

github

web-flow
Merge a3b5e34d5 into 08fadfed4
Pull Request #516: fix: add teams to exception when determining function name (box/box-codegen#663)

4049 of 16047 branches covered (25.23%)

Branch coverage included in aggregate %.

14320 of 27065 relevant lines covered (52.91%)

92.48 hits per line

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

82.05
/src/networking/boxNetworkClient.ts
1
import nodeFetch, { RequestInit } from 'node-fetch';
144✔
2
import type { Readable } from 'stream';
3
import { sha1 } from 'hash-wasm'; // Use hash-wasm to calculate SHA1 hash in browser
144✔
4

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

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

32
export interface MultipartItem {
33
  readonly partName: string;
34
  readonly data?: SerializedData;
35
  readonly fileStream?: ByteStream;
36
  readonly fileName?: string;
37
  readonly contentType?: string;
38
}
39

40
type FetchOptionsExtended = FetchOptions & {
41
  numRetries?: number;
42
};
43

44
async function createRequestInit(options: FetchOptions): Promise<RequestInit> {
45
  const {
46
    method = 'GET',
×
47
    headers = {},
14✔
48
    contentType: contentTypeInput = 'application/json',
×
49
    data,
50
    fileStream,
51
  } = options;
1,616✔
52

53
  const { contentHeaders = {}, body } = await (async (): Promise<{
1,616!
54
    contentHeaders: { [key: string]: string };
55
    body: Readable | string | Buffer;
56
  }> => {
1,616✔
57
    const contentHeaders: { [key: string]: string } = {};
1,616✔
58
    if (options.multipartData) {
1,616✔
59
      const FormData = isBrowser() ? window.FormData : evalRequire('form-data');
116!
60
      const formData = new FormData();
116✔
61
      for (const item of options.multipartData) {
116✔
62
        if (item.fileStream) {
230✔
63
          const buffer = await readByteStream(item.fileStream);
116✔
64
          const blob = isBrowser() ? new Blob([buffer]) : buffer;
116!
65
          contentHeaders['content-md5'] = await calculateMD5Hash(buffer);
116✔
66
          formData.append(item.partName, blob, {
116✔
67
            filename: item.fileName ?? 'file',
348✔
68
            contentType: item.contentType ?? 'application/octet-stream',
348✔
69
          });
70
        } else if (item.data) {
114!
71
          formData.append(item.partName, sdToJson(item.data));
114✔
72
        } else {
73
          throw new BoxSdkError({
×
74
            message: 'Multipart item must have either body or fileStream',
75
          });
76
        }
77
      }
78
      return {
116✔
79
        contentHeaders: {
80
          ...(!isBrowser() && {
232✔
81
            'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`,
82
          }),
83
          ...contentHeaders,
84
        },
85
        body: formData,
86
      };
87
    }
88

89
    contentHeaders['Content-Type'] = contentTypeInput;
1,500✔
90
    switch (contentTypeInput) {
1,500!
91
      case 'application/json':
92
      case 'application/json-patch+json':
93
        return { contentHeaders, body: sdToJson(data) };
1,188✔
94

95
      case 'application/x-www-form-urlencoded':
96
        return { contentHeaders, body: sdToUrlParams(data) };
294✔
97

98
      case 'application/octet-stream':
99
        if (!fileStream) {
18!
100
          throw new BoxSdkError({
×
101
            message:
102
              'fileStream required for application/octet-stream content type',
103
          });
104
        }
105
        return {
18✔
106
          contentHeaders,
107
          body: isBrowser() ? await readByteStream(fileStream) : fileStream,
18!
108
        };
109

110
      default:
111
        throw new BoxSdkError({
×
112
          message: `Unsupported content type : ${contentTypeInput}`,
113
        });
114
    }
115
  })();
116

117
  return {
1,616✔
118
    method,
119
    headers: {
120
      ...contentHeaders,
121
      ...headers,
122
      ...(options.auth && {
2,928✔
123
        Authorization: await options.auth.retrieveAuthorizationHeader(
124
          options.networkSession,
125
        ),
126
      }),
127
      'User-Agent': userAgentHeader,
128
      'X-Box-UA': xBoxUaHeader,
129
      // Additional headers will override the default headers
130
      ...options.networkSession?.additionalHeaders,
4,848!
131
    },
132
    body,
133
    signal: options.cancellationToken as RequestInit['signal'],
134
    agent: options.networkSession?.agent,
4,848!
135
    ...(fileStream && isBrowser() && { duplex: 'half' }),
1,634!
136
  };
137
}
138

139
export class BoxNetworkClient implements NetworkClient {
144✔
140
  constructor(
141
    fields?: Omit<BoxNetworkClient, 'fetch'> &
142
      Partial<Pick<BoxNetworkClient, 'fetch'>>,
143
  ) {
144
    Object.assign(this, fields);
23,122✔
145
  }
146
  async fetch(options: FetchOptionsExtended): Promise<FetchResponse> {
147
    const numRetries = options.numRetries ?? 0;
1,616!
148
    const networkSession = options.networkSession ?? new NetworkSession({});
1,616!
149
    const fetchOptions: typeof options = networkSession.interceptors?.length
1,616!
150
      ? networkSession.interceptors.reduce(
151
          (modifiedOptions: FetchOptions, interceptor: Interceptor) =>
152
            interceptor.beforeRequest(modifiedOptions),
10✔
153
          options,
154
        )
155
      : options;
156
    const fileStreamBuffer = fetchOptions.fileStream
1,616✔
157
      ? await readByteStream(fetchOptions.fileStream)
158
      : void 0;
159
    const requestInit = await createRequestInit({
1,616✔
160
      ...fetchOptions,
161
      fileStream: fileStreamBuffer
1,616✔
162
        ? generateByteStreamFromBuffer(fileStreamBuffer)
163
        : void 0,
164
    });
165

166
    const { params = {} } = fetchOptions;
1,616✔
167
    const response = await nodeFetch(
1,616✔
168
      ''.concat(
169
        fetchOptions.url,
170
        Object.keys(params).length === 0 || fetchOptions.url.endsWith('?')
3,346✔
171
          ? ''
172
          : '?',
173
        new URLSearchParams(params).toString(),
174
      ),
175
      { ...requestInit, redirect: isBrowser() ? 'follow' : 'manual' },
1,616!
176
    );
177

178
    const contentType = response.headers.get('content-type') ?? '';
1,614✔
179
    const ignoreResponseBody = fetchOptions.followRedirects === false;
1,614✔
180
    const responseBytesBuffer = !ignoreResponseBody
1,614✔
181
      ? await response.arrayBuffer()
182
      : new Uint8Array();
183

184
    const data = ((): SerializedData => {
1,614✔
185
      if (!ignoreResponseBody && contentType.includes('application/json')) {
1,614✔
186
        const text = new TextDecoder().decode(responseBytesBuffer);
1,232✔
187
        return jsonToSerializedData(text);
1,232✔
188
      }
189
      return void 0;
382✔
190
    })();
191

192
    const content = generateByteStreamFromBuffer(responseBytesBuffer);
1,614✔
193

194
    let fetchResponse: FetchResponse = {
1,614✔
195
      url: response.url,
196
      status: response.status,
197
      data,
198
      content,
199
      headers: Object.fromEntries(Array.from(response.headers.entries())),
200
    };
201
    if (networkSession.interceptors?.length) {
1,614!
202
      fetchResponse = networkSession.interceptors.reduce(
8✔
203
        (modifiedResponse: FetchResponse, interceptor: Interceptor) =>
204
          interceptor.afterRequest(modifiedResponse),
10✔
205
        fetchResponse,
206
      );
207
    }
208

209
    const shouldRetry = await networkSession.retryStrategy.shouldRetry(
1,614✔
210
      fetchOptions,
211
      fetchResponse,
212
      numRetries,
213
    );
214

215
    if (shouldRetry) {
1,612!
216
      const retryTimeout = networkSession.retryStrategy.retryAfter(
×
217
        fetchOptions,
218
        fetchResponse,
219
        numRetries,
220
      );
221
      await new Promise((resolve) => setTimeout(resolve, retryTimeout));
×
222
      return this.fetch({ ...options, numRetries: numRetries + 1 });
×
223
    }
224

225
    if (
1,612✔
226
      fetchResponse.status >= 300 &&
1,740✔
227
      fetchResponse.status < 400 &&
228
      fetchOptions.followRedirects !== false
229
    ) {
230
      if (!fetchResponse.headers['location']) {
14✔
231
        throw new BoxSdkError({
2✔
232
          message: `Unable to follow redirect for ${fetchOptions.url}`,
233
        });
234
      }
235
      const sameOrigin =
236
        new URL(fetchResponse.headers['location']).origin ===
12✔
237
        new URL(fetchOptions.url).origin;
238
      return this.fetch({
10✔
239
        ...options,
240
        params: undefined,
241
        auth: sameOrigin ? fetchOptions.auth : undefined,
10!
242
        url: fetchResponse.headers['location'],
243
      });
244
    }
245

246
    if (fetchResponse.status >= 200 && fetchResponse.status < 400) {
1,598✔
247
      return fetchResponse;
1,506✔
248
    }
249

250
    const [code, contextInfo, requestId, helpUrl] = sdIsMap(fetchResponse.data)
92✔
251
      ? [
252
          sdToJson(fetchResponse.data['code']),
253
          sdIsMap(fetchResponse.data['context_info'])
76✔
254
            ? fetchResponse.data['context_info']
255
            : undefined,
256
          sdToJson(fetchResponse.data['request_id']),
257
          sdToJson(fetchResponse.data['help_url']),
258
        ]
259
      : [];
260

261
    throw new BoxApiError({
92✔
262
      message: `${fetchResponse.status}`,
263
      timestamp: `${Date.now()}`,
264
      requestInfo: {
265
        method: requestInit.method!,
266
        url: fetchOptions.url,
267
        queryParams: params,
268
        headers: (requestInit.headers as { [key: string]: string }) ?? {},
276!
269
        body:
270
          typeof requestInit.body === 'string' ? requestInit.body : undefined,
92✔
271
      },
272
      responseInfo: {
273
        statusCode: fetchResponse.status,
274
        headers: fetchResponse.headers,
275
        body: fetchResponse.data,
276
        rawBody: new TextDecoder().decode(responseBytesBuffer),
277
        code: code,
278
        contextInfo: contextInfo,
279
        requestId: requestId,
280
        helpUrl: helpUrl,
281
      },
282
    });
283
  }
284
}
285

286
async function calculateMD5Hash(data: string | Buffer): Promise<string> {
287
  /**
288
   * Calculate the SHA1 hash of the data
289
   */
290
  let createHash: any;
291
  // Browser environment
292
  if (isBrowser()) {
116!
293
    return await sha1(data);
×
294
  }
295

296
  // Node environment
297
  createHash = evalRequire('crypto').createHash;
116✔
298
  return createHash('sha1').update(data).digest('hex');
116✔
299
}
300

301
function constructBoxUAHeader() {
302
  const analyticsIdentifiers = {
144✔
303
    agent: `box-javascript-generated-sdk/${sdkVersion}`,
304
    env: isBrowser()
144!
305
      ? navigator.userAgent
306
      : `Node/${process.version.replace('v', '')}`,
307
  } as Record<string, string>;
308

309
  return Object.keys(analyticsIdentifiers)
144✔
310
    .map((k) => `${k}=${analyticsIdentifiers[k]}`)
288✔
311
    .join('; ');
312
}
313

314
// Retry intervals are between 50% and 150% of the exponentially increasing base amount
315
const RETRY_RANDOMIZATION_FACTOR = 0.5;
144✔
316

317
/**
318
 * Calculate the exponential backoff time with randomized jitter
319
 * @param {int} numRetries Which retry number this one will be
320
 * @param {int} baseInterval The base retry interval set in config
321
 * @returns {int} The number of milliseconds after which to retry
322
 */
323
export function getRetryTimeout(numRetries: number, baseInterval: number) {
144✔
324
  var minRandomization = 1 - RETRY_RANDOMIZATION_FACTOR;
×
325
  var maxRandomization = 1 + RETRY_RANDOMIZATION_FACTOR;
×
326
  var randomization =
327
    Math.random() * (maxRandomization - minRandomization) + minRandomization;
×
328
  var exponential = Math.pow(2, numRetries - 1);
×
329
  return Math.ceil(exponential * baseInterval * randomization);
×
330
}
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