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

box / box-typescript-sdk-gen / 8287132596

14 Mar 2024 08:24PM UTC coverage: 43.903% (+1.9%) from 42.003%
8287132596

Pull #95

github

web-flow
Merge bfcd4a35b into 14e115481
Pull Request #95: feat: use getDiscriminatorsForUnion in generic serialization (box/box-codegen#448)

2293 of 9072 branches covered (25.28%)

Branch coverage included in aggregate %.

347 of 405 new or added lines in 17 files covered. (85.68%)

187 existing lines in 6 files now uncovered.

7226 of 12610 relevant lines covered (57.3%)

68.18 hits per line

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

80.75
/src/networking/fetch.ts
1
import nodeFetch, { RequestInit } from 'node-fetch';
136✔
2
import type { Readable } from 'stream';
3

4
import { BoxApiError, BoxSdkError } from '../box/errors';
136✔
5
import {
136✔
6
  ByteStream,
7
  CancellationToken,
8
  generateByteStreamFromBuffer,
9
  isBrowser,
10
} from '../internal/utils';
11
import { sdkVersion } from './version';
136✔
12
import {
136✔
13
  SerializedData,
14
  jsonToSerializedData,
15
  sdIsMap,
16
  sdToJson,
17
  sdToUrlParams,
18
} from '../serialization/json';
19
import { Authentication } from './auth.generated';
20
import { getRetryTimeout } from './getRetryTimeout';
136✔
21
import { Interceptor } from './interceptors.generated';
22
import { NetworkSession } from './network.generated';
23

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

29
export interface MultipartItem {
30
  readonly partName: string;
31
  readonly data?: SerializedData;
32
  readonly fileStream?: ByteStream;
33
  readonly fileName?: string;
34
  readonly contentType?: string;
35
}
36

37
export interface FetchOptions {
38
  /**
39
   * A string to set request's method (GET, POST, etc.). Defaults to GET.
40
   */
41
  readonly method?: string;
42
  /**
43
   * [key1, value1, key2, value2, ...]
44
   */
45
  readonly headers?: {
46
    [key: string]: string;
47
  };
48
  /**
49
   * query params
50
   * [key1, value1, key2, value2, ...]
51
   */
52
  readonly params?: {
53
    [key: string]: string;
54
  };
55

56
  /**
57
   * Request body data
58
   */
59
  readonly data?: SerializedData;
60

61
  /**
62
   * Stream of a file
63
   */
64
  readonly fileStream?: ByteStream;
65

66
  /**
67
   * Parts of multipart data
68
   */
69
  readonly multipartData?: MultipartItem[];
70

71
  /**
72
   * Request body content type
73
   */
74
  readonly contentType?: string;
75

76
  /**
77
   * Expected format of the response: 'json', 'binary' or undefined
78
   */
79
  readonly responseFormat?: string;
80

81
  /**
82
   * Auth object
83
   */
84
  readonly auth?: Authentication;
85
  /**
86
   *
87
   */
88
  readonly networkSession?: NetworkSession;
89

90
  /**
91
   * Token used for request cancellation
92
   */
93
  readonly cancellationToken?: CancellationToken;
94
}
95

96
export interface FetchResponse {
97
  /**
98
   * The status code of the response. (This will be 200 for a success).
99
   */
100
  readonly status: number;
101

102
  /**
103
   * Response body data
104
   */
105
  readonly data: SerializedData;
106

107
  /**
108
   * Binary array buffer of response body
109
   */
110
  readonly content: ByteStream;
111

112
  readonly headers: {
113
    [key: string]: string;
114
  };
115
}
116

117
async function createRequestInit(options: FetchOptions): Promise<RequestInit> {
118
  const {
119
    method = 'GET',
×
120
    headers = {},
×
121
    contentType: contentTypeInput = 'application/json',
676✔
122
    data,
123
    fileStream,
124
  } = options;
1,306✔
125

126
  const { contentType, body } = await (async (): Promise<{
1,306✔
127
    contentType: string;
128
    body: Readable | string;
129
  }> => {
1,306✔
130
    if (options.multipartData) {
1,306✔
131
      const FormData = isBrowser()
80!
132
        ? window.FormData
133
        : eval('require')('form-data');
134
      const formData = new FormData();
80✔
135
      for (const item of options.multipartData) {
80✔
136
        if (item.fileStream) {
158✔
137
          const buffer = await readStream(item.fileStream);
80✔
138
          headers['content-md5'] = await calculateMD5Hash(buffer);
80✔
139
          formData.append(item.partName, buffer, {
80✔
140
            filename: item.fileName ?? 'file',
240✔
141
            contentType: item.contentType ?? 'application/octet-stream',
240✔
142
          });
143
        } else if (item.data) {
78!
144
          formData.append(item.partName, sdToJson(item.data));
78✔
145
        } else {
146
          throw new BoxSdkError({
×
147
            message: 'Multipart item must have either body or fileStream',
148
          });
149
        }
150
      }
151

152
      return {
80✔
153
        contentType: `multipart/form-data; boundary=${formData.getBoundary()}`,
154
        body: formData,
155
      };
156
    }
157

158
    const contentType = contentTypeInput;
1,226✔
159
    switch (contentType) {
1,226!
160
      case 'application/json':
161
      case 'application/json-patch+json':
162
        return { contentType, body: sdToJson(data) };
972✔
163

164
      case 'application/x-www-form-urlencoded':
165
        return { contentType, body: sdToUrlParams(data) };
248✔
166

167
      case 'application/octet-stream':
168
        if (!fileStream) {
6!
169
          throw new BoxSdkError({
×
170
            message:
171
              'fileStream required for application/octet-stream content type',
172
          });
173
        }
174
        return { contentType, body: fileStream };
6✔
175

176
      default:
177
        throw new BoxSdkError({
×
178
          message: `Unsupported content type : ${contentType}`,
179
        });
180
    }
181
  })();
182

183
  return {
1,306✔
184
    method,
185
    headers: {
186
      ...options.networkSession?.additionalHeaders,
3,918!
187
      'Content-Type': contentType,
188
      ...headers,
189
      ...(options.auth && {
2,364✔
190
        Authorization: await options.auth.retrieveAuthorizationHeader(
191
          options.networkSession
192
        ),
193
      }),
194
      'User-Agent': userAgentHeader,
195
      'X-Box-UA': xBoxUaHeader,
196
    },
197
    body,
198
    signal: options.cancellationToken as RequestInit['signal'],
199
  };
200
}
201

202
const DEFAULT_MAX_ATTEMPTS = 5;
136✔
203
const RETRY_BASE_INTERVAL = 1;
136✔
204

205
export async function fetch(
136✔
206
  resource: string,
207
  options: FetchOptions & {
208
    /** @private */
209
    numRetries?: number;
210
  }
211
): Promise<FetchResponse> {
212
  const fetchOptions: typeof options = options.networkSession?.interceptors
1,306!
213
    ?.length
214
    ? options.networkSession?.interceptors.reduce(
30!
215
        (modifiedOptions: FetchOptions, interceptor: Interceptor) =>
216
          interceptor.beforeRequest(modifiedOptions),
12✔
217
        options
218
      )
219
    : options;
220

221
  const requestInit = await createRequestInit(fetchOptions);
1,306✔
222

223
  const { params = {} } = fetchOptions;
1,306✔
224
  const response = await nodeFetch(
1,306✔
225
    ''.concat(
226
      resource,
227
      Object.keys(params).length === 0 || resource.endsWith('?') ? '' : '?',
2,702✔
228
      new URLSearchParams(params).toString()
229
    ),
230
    { ...requestInit, redirect: 'manual' }
231
  );
232

233
  const contentType = response.headers.get('content-type') ?? '';
1,302✔
234
  const responseBytesBuffer = await response.arrayBuffer();
1,302✔
235

236
  const data = ((): SerializedData => {
1,302✔
237
    if (contentType.includes('application/json')) {
1,302✔
238
      const text = new TextDecoder().decode(responseBytesBuffer);
1,016✔
239
      return jsonToSerializedData(text);
1,016✔
240
    }
241
    return void 0;
286✔
242
  })();
243

244
  const content = generateByteStreamFromBuffer(responseBytesBuffer);
1,302✔
245

246
  let fetchResponse: FetchResponse = {
1,302✔
247
    status: response.status,
248
    data,
249
    content,
250
    headers: Object.fromEntries(Array.from(response.headers.entries())),
251
  };
252
  if (fetchOptions.networkSession?.interceptors?.length) {
1,302!
253
    fetchResponse = fetchOptions.networkSession?.interceptors.reduce(
8!
254
      (modifiedResponse: FetchResponse, interceptor: Interceptor) =>
255
        interceptor.afterRequest(modifiedResponse),
10✔
256
      fetchResponse
257
    );
258
  }
259

260
  if (fetchResponse.status >= 300 && fetchResponse.status < 400) {
1,302✔
261
    if (!fetchResponse.headers['location']) {
10✔
262
      throw new BoxSdkError({
2✔
263
        message: `Unable to follow redirect for ${resource}`,
264
      });
265
    }
266
    return fetch(fetchResponse.headers['location'], options);
8✔
267
  }
268

269
  if (fetchResponse.status >= 400) {
1,292✔
270
    const { numRetries = 0 } = fetchOptions;
90✔
271

272
    const reauthenticationNeeded = fetchResponse.status == 401;
90✔
273
    if (reauthenticationNeeded && fetchOptions.auth) {
90✔
274
      await fetchOptions.auth.refreshToken(fetchOptions.networkSession);
2✔
275

276
      // retry the request right away
UNCOV
277
      return fetch(resource, { ...fetchOptions, numRetries: numRetries + 1 });
×
278
    }
279

280
    const isRetryable =
281
      fetchOptions.contentType !== 'application/x-www-form-urlencoded' &&
88✔
282
      (fetchResponse.status === 429 || fetchResponse.status >= 500);
283

284
    if (isRetryable && numRetries < DEFAULT_MAX_ATTEMPTS) {
88!
285
      const retryTimeout = fetchResponse.headers['retry-after']
×
286
        ? parseFloat(fetchResponse.headers['retry-after']!) * 1000
287
        : getRetryTimeout(numRetries, RETRY_BASE_INTERVAL * 1000);
288

289
      await new Promise((resolve) => setTimeout(resolve, retryTimeout));
×
290
      return fetch(resource, { ...fetchOptions, numRetries: numRetries + 1 });
×
291
    }
292

293
    const [code, contextInfo, requestId, helpUrl] = sdIsMap(fetchResponse.data)
88✔
294
      ? [
295
          sdToJson(fetchResponse.data['code']),
296
          sdIsMap(fetchResponse.data['context_info'])
76✔
297
            ? fetchResponse.data['context_info']
298
            : undefined,
299
          sdToJson(fetchResponse.data['request_id']),
300
          sdToJson(fetchResponse.data['help_url']),
301
        ]
302
      : [];
303

304
    throw new BoxApiError({
88✔
305
      message: `${fetchResponse.status}`,
306
      timestamp: `${Date.now()}`,
307
      requestInfo: {
308
        method: requestInit.method!,
309
        url: resource,
310
        queryParams: params,
311
        headers: (requestInit.headers as { [key: string]: string }) ?? {},
264!
312
        body: requestInit.body,
313
      },
314
      responseInfo: {
315
        statusCode: fetchResponse.status,
316
        headers: fetchResponse.headers,
317
        body: fetchResponse.data,
318
        rawBody: new TextDecoder().decode(responseBytesBuffer),
319
        code: code,
320
        contextInfo: contextInfo,
321
        requestId: requestId,
322
        helpUrl: helpUrl,
323
      },
324
      name: 'BoxApiError',
325
    });
326
  }
327

328
  return fetchResponse;
1,202✔
329
}
330

331
async function calculateMD5Hash(data: string | Buffer): Promise<string> {
332
  /**
333
   * Calculate the SHA1 hash of the data
334
   */
335
  let createHash: any;
336
  // Browser environment
337
  if (isBrowser()) {
80!
338
    let dataBuffer =
339
      typeof data === 'string' ? new TextEncoder().encode(data) : data;
×
340
    let hashBuffer = await window.crypto.subtle.digest('SHA-1', dataBuffer);
×
341
    let hashArray = Array.from(new Uint8Array(hashBuffer));
×
342
    let hashHex = hashArray
×
343
      .map((b) => b.toString(16).padStart(2, '0'))
×
344
      .join('');
345
    return hashHex;
×
346
  }
347

348
  // Node environment
349
  createHash = eval('require')('crypto').createHash;
80✔
350
  return createHash('sha1').update(data).digest('hex');
80✔
351
}
352

353
async function readStream(fileStream: Readable): Promise<Buffer> {
354
  return new Promise((resolve, reject) => {
80✔
355
    const chunks: any[] = [];
80✔
356
    fileStream.on('data', (chunk: Buffer) => chunks.push(chunk));
80✔
357
    fileStream.on('end', () => {
80✔
358
      resolve(Buffer.concat(chunks));
80✔
359
    });
360
    fileStream.on('error', (err: Error) => reject(err));
80✔
361
  });
362
}
363

364
function constructBoxUAHeader() {
365
  const analyticsIdentifiers = {
136✔
366
    agent: `box-javascript-generated-sdk/${sdkVersion}`,
367
    env: isBrowser()
136!
368
      ? navigator.userAgent
369
      : `Node/${process.version.replace('v', '')}`,
370
  } as Record<string, string>;
371

372
  return Object.keys(analyticsIdentifiers)
136✔
373
    .map((k) => `${k}=${analyticsIdentifiers[k]}`)
272✔
374
    .join('; ');
375
}
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

© 2025 Coveralls, Inc