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

box / box-typescript-sdk-gen / 16053507753

03 Jul 2025 02:42PM UTC coverage: 42.057% (+0.009%) from 42.048%
16053507753

Pull #657

github

web-flow
Merge b057fa921 into cb0c35df4
Pull Request #657: feat: Add CI for swift (box/box-codegen#755)

4364 of 18050 branches covered (24.18%)

Branch coverage included in aggregate %.

16052 of 30494 relevant lines covered (52.64%)

147.15 hits per line

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

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

3
import { BoxApiError, BoxSdkError } from '../box/errors';
234✔
4
import {
234✔
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';
234✔
15
import { NetworkClient } from './networkClient.generated';
16
import {
234✔
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 { RetryStrategy } from './retries.generated.js';
27
import { BoxRetryStrategy } from './retries.generated.js';
234✔
28
import { DataSanitizer } from '../internal/logging.generated.js';
234✔
29

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

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

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

51
type FetchOptionsExtended = FetchOptions & {
52
  numRetries?: number;
53
};
54

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

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

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

106
      case 'application/x-www-form-urlencoded':
107
        return { contentHeaders, body: sdToUrlParams(data) };
487✔
108

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

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

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

155
export class BoxNetworkClient implements NetworkClient {
234✔
156
  constructor(
157
    fields?: Omit<BoxNetworkClient, 'fetch'> &
158
      Partial<Pick<BoxNetworkClient, 'fetch'>>,
159
  ) {
160
    Object.assign(this, fields);
40,048✔
161
  }
162
  async fetch(options: FetchOptionsExtended): Promise<FetchResponse> {
163
    const numRetries = options.numRetries ?? 0;
2,680!
164
    const interceptors: readonly Interceptor[] =
165
      options.networkSession?.interceptors ?? [];
2,680!
166
    const retryStrategy: RetryStrategy =
167
      options.networkSession?.retryStrategy ?? new BoxRetryStrategy({});
2,680!
168
    const dataSanitizer: DataSanitizer =
169
      options.networkSession?.dataSanitizer ?? new DataSanitizer({});
2,680!
170
    const fetchOptions: typeof options = interceptors.length
2,680✔
171
      ? interceptors.reduce(
172
          (modifiedOptions: FetchOptions, interceptor: Interceptor) =>
173
            interceptor.beforeRequest(modifiedOptions),
15✔
174
          options,
175
        )
176
      : options;
177
    const fileStreamBuffer = fetchOptions.fileStream
2,680✔
178
      ? await readByteStream(fetchOptions.fileStream)
179
      : void 0;
180
    const multipartBuffer = fetchOptions.multipartData
2,680✔
181
      ? await multipartStreamToBuffer(fetchOptions.multipartData)
182
      : void 0;
183

184
    const requestInit = await createRequestInit({
2,680✔
185
      ...fetchOptions,
186
      fileStream: fileStreamBuffer
2,680✔
187
        ? generateByteStreamFromBuffer(fileStreamBuffer)
188
        : void 0,
189
      multipartData: multipartBuffer
2,680✔
190
        ? multipartBufferToStream(multipartBuffer)
191
        : void 0,
192
    });
193

194
    const { params = {} } = fetchOptions;
2,680✔
195
    const response = await nodeFetch(
2,680✔
196
      ''.concat(
197
        fetchOptions.url,
198
        Object.keys(params).length === 0 || fetchOptions.url.endsWith('?')
5,570✔
199
          ? ''
200
          : '?',
201
        new URLSearchParams(params).toString(),
202
      ),
203
      { ...requestInit, redirect: isBrowser() ? 'follow' : 'manual' },
2,680!
204
    );
205

206
    const contentType = response.headers.get('content-type') ?? '';
2,677✔
207
    const ignoreResponseBody = fetchOptions.followRedirects === false;
2,677✔
208

209
    let data: SerializedData | undefined;
210
    let content: ByteStream = generateByteStreamFromBuffer(new Uint8Array());
2,677✔
211
    let responseBytesBuffer: ArrayBuffer | undefined;
212

213
    if (!ignoreResponseBody) {
2,677✔
214
      if (options.responseFormat === 'binary') {
2,671✔
215
        content = response.body as unknown as ByteStream;
39✔
216
        responseBytesBuffer = new Uint8Array();
39✔
217
      } else if (options.responseFormat === 'json') {
2,632✔
218
        responseBytesBuffer = await response.arrayBuffer();
2,053✔
219
        const text = new TextDecoder().decode(responseBytesBuffer);
2,053✔
220
        if (contentType.includes('application/json')) {
2,053✔
221
          data = jsonToSerializedData(text);
2,023✔
222
        }
223
        content = generateByteStreamFromBuffer(responseBytesBuffer);
2,053✔
224
      }
225
    }
226

227
    let fetchResponse: FetchResponse = {
2,677✔
228
      url: response.url,
229
      status: response.status,
230
      data,
231
      content,
232
      headers: Object.fromEntries(Array.from(response.headers.entries())),
233
    };
234
    if (interceptors.length) {
2,677✔
235
      fetchResponse = interceptors.reduce(
12✔
236
        (modifiedResponse: FetchResponse, interceptor: Interceptor) =>
237
          interceptor.afterRequest(modifiedResponse),
15✔
238
        fetchResponse,
239
      );
240
    }
241

242
    const shouldRetry = await retryStrategy.shouldRetry(
2,677✔
243
      fetchOptions,
244
      fetchResponse,
245
      numRetries,
246
    );
247

248
    if (shouldRetry) {
2,674!
249
      const retryTimeout = retryStrategy.retryAfter(
×
250
        fetchOptions,
251
        fetchResponse,
252
        numRetries,
253
      );
254
      await new Promise((resolve) => setTimeout(resolve, retryTimeout));
×
255
      return this.fetch({
×
256
        ...options,
257
        numRetries: numRetries + 1,
258
        fileStream: fileStreamBuffer
×
259
          ? generateByteStreamFromBuffer(fileStreamBuffer)
260
          : void 0,
261
        multipartData: multipartBuffer
×
262
          ? multipartBufferToStream(multipartBuffer)
263
          : void 0,
264
      });
265
    }
266

267
    if (
2,674✔
268
      fetchResponse.status >= 300 &&
2,894✔
269
      fetchResponse.status < 400 &&
270
      fetchOptions.followRedirects !== false
271
    ) {
272
      if (!fetchResponse.headers['location']) {
21✔
273
        throw new BoxSdkError({
3✔
274
          message: `Unable to follow redirect for ${fetchOptions.url}`,
275
        });
276
      }
277
      const sameOrigin =
278
        new URL(fetchResponse.headers['location']).origin ===
18✔
279
        new URL(fetchOptions.url).origin;
280
      return this.fetch({
15✔
281
        ...options,
282
        params: undefined,
283
        auth: sameOrigin ? fetchOptions.auth : undefined,
15!
284
        url: fetchResponse.headers['location'],
285
      });
286
    }
287

288
    if (fetchResponse.status >= 200 && fetchResponse.status < 400) {
2,653✔
289
      return fetchResponse;
2,487✔
290
    }
291

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

306
    throw new BoxApiError({
166✔
307
      message: `${fetchResponse.status} ${message}; Request ID: ${requestId}`,
308
      timestamp: `${Date.now()}`,
309
      requestInfo: {
310
        method: requestInit.method!,
311
        url: fetchOptions.url,
312
        queryParams: params,
313
        headers: (requestInit.headers as { [key: string]: string }) ?? {},
498!
314
        body:
315
          typeof requestInit.body === 'string' ? requestInit.body : undefined,
166✔
316
      },
317
      responseInfo: {
318
        statusCode: fetchResponse.status,
319
        headers: fetchResponse.headers,
320
        body: fetchResponse.data,
321
        rawBody: new TextDecoder().decode(responseBytesBuffer),
322
        code: code,
323
        contextInfo: contextInfo,
324
        requestId: requestId,
325
        helpUrl: helpUrl,
326
      },
327
      dataSanitizer: dataSanitizer,
328
    });
329
  }
330
}
331

332
function constructBoxUAHeader() {
333
  const analyticsIdentifiers = {
234✔
334
    agent: `box-javascript-generated-sdk/${sdkVersion}`,
335
    env: isBrowser()
234!
336
      ? navigator.userAgent
337
      : `Node/${process.version.replace('v', '')}`,
338
  } as Record<string, string>;
339

340
  return Object.keys(analyticsIdentifiers)
234✔
341
    .map((k) => `${k}=${analyticsIdentifiers[k]}`)
468✔
342
    .join('; ');
343
}
344

345
// Retry intervals are between 50% and 150% of the exponentially increasing base amount
346
const RETRY_RANDOMIZATION_FACTOR = 0.5;
234✔
347

348
/**
349
 * Calculate the exponential backoff time with randomized jitter
350
 * @param {int} numRetries Which retry number this one will be
351
 * @param {int} baseInterval The base retry interval set in config
352
 * @returns {int} The number of milliseconds after which to retry
353
 */
354
export function getRetryTimeout(numRetries: number, baseInterval: number) {
234✔
355
  var minRandomization = 1 - RETRY_RANDOMIZATION_FACTOR;
×
356
  var maxRandomization = 1 + RETRY_RANDOMIZATION_FACTOR;
×
357
  var randomization =
358
    Math.random() * (maxRandomization - minRandomization) + minRandomization;
×
359
  var exponential = Math.pow(2, numRetries - 1);
×
360
  return Math.ceil(exponential * baseInterval * randomization);
×
361
}
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