fetch-har 9.0.0 → 11.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/example.js DELETED
@@ -1,41 +0,0 @@
1
- require('isomorphic-fetch');
2
-
3
- // If executing from an environment that doesn't normally provide `fetch()`
4
- // we'll automatically polyfill in the `Blob`, `File`, and `FormData` APIs
5
- // with the optional `formdata-node` package (provided you've installed it).
6
- const fetchHAR = require('.').default;
7
-
8
- const har = {
9
- log: {
10
- entries: [
11
- {
12
- request: {
13
- headers: [
14
- {
15
- name: 'Authorization',
16
- value: 'Bearer api-key',
17
- },
18
- {
19
- name: 'Content-Type',
20
- value: 'application/json',
21
- },
22
- ],
23
- queryString: [
24
- { name: 'a', value: 1 },
25
- { name: 'b', value: 2 },
26
- ],
27
- postData: {
28
- mimeType: 'application/json',
29
- text: '{"id":8,"category":{"id":6,"name":"name"},"name":"name"}',
30
- },
31
- method: 'POST',
32
- url: 'http://httpbin.org/post',
33
- },
34
- },
35
- ],
36
- },
37
- };
38
-
39
- fetchHAR(har)
40
- .then(res => res.json())
41
- .then(console.log);
package/src/index.ts DELETED
@@ -1,432 +0,0 @@
1
- import type { DataURL as npmDataURL } from '@readme/data-urls';
2
- import type { Har } from 'har-format';
3
-
4
- import { parse as parseDataUrl } from '@readme/data-urls';
5
- import { Readable } from 'readable-stream';
6
-
7
- if (!globalThis.Blob) {
8
- try {
9
- // eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-extraneous-dependencies
10
- globalThis.Blob = require('formdata-node').Blob;
11
- } catch (e) {
12
- throw new Error(
13
- 'Since you do not have the Blob API available in this environment you must install the optional `formdata-node` dependency.'
14
- );
15
- }
16
- }
17
-
18
- if (!globalThis.File) {
19
- try {
20
- // eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-extraneous-dependencies
21
- globalThis.File = require('formdata-node').File;
22
- } catch (e) {
23
- throw new Error(
24
- 'Since you do not have the File API available in this environment you must install the optional `formdata-node` dependency.'
25
- );
26
- }
27
- }
28
-
29
- if (!globalThis.FormData) {
30
- try {
31
- // eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-extraneous-dependencies
32
- globalThis.FormData = require('formdata-node').FormData;
33
- } catch (e) {
34
- throw new Error(
35
- 'Since you do not have the FormData API available in this environment you must install the optional `formdata-node` dependency.'
36
- );
37
- }
38
- }
39
-
40
- interface RequestInitWithDuplex extends RequestInit {
41
- /**
42
- * `RequestInit#duplex` does not yet exist in the TS `lib.dom.d.ts` definition yet the native
43
- * fetch implementation in Node 18+, `undici`, requires it for certain POST payloads.
44
- *
45
- * @see {@link https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1483}
46
- * @see {@link https://github.com/nodejs/node/issues/46221}
47
- * @see {@link https://fetch.spec.whatwg.org/#request-class}
48
- * @see {@link https://github.com/microsoft/TypeScript/blob/main/lib/lib.dom.d.ts}
49
- */
50
- duplex?: 'half';
51
- }
52
-
53
- export interface FetchHAROptions {
54
- userAgent?: string;
55
- files?: Record<string, Blob | Buffer>;
56
- multipartEncoder?: any; // form-data-encoder
57
- init?: RequestInitWithDuplex;
58
- }
59
-
60
- type DataURL = npmDataURL & {
61
- // `parse-data-url` doesn't explicitly support `name` in data URLs but if it's there it'll be
62
- // returned back to us.
63
- name?: string;
64
- };
65
-
66
- function isBrowser() {
67
- return typeof window !== 'undefined' && typeof document !== 'undefined';
68
- }
69
-
70
- function isBuffer(value: any) {
71
- return typeof Buffer !== 'undefined' && Buffer.isBuffer(value);
72
- }
73
-
74
- function isFile(value: any) {
75
- if (value instanceof File) {
76
- /**
77
- * The `Blob` polyfill on Node comes back as being an instanceof `File`. Because passing a Blob
78
- * into a File will end up with a corrupted file we want to prevent this.
79
- *
80
- * This object identity crisis does not happen in the browser.
81
- */
82
- return value.constructor.name === 'File';
83
- }
84
-
85
- return false;
86
- }
87
-
88
- /**
89
- * @license MIT
90
- * @see {@link https://github.com/octet-stream/form-data-encoder/blob/master/lib/util/isFunction.ts}
91
- */
92
- function isFunction(value: any) {
93
- return typeof value === 'function';
94
- }
95
-
96
- /**
97
- * We're using this library in here instead of loading it from `form-data-encoder` because that
98
- * uses lookbehind regex in its main encoder that Safari doesn't support so it throws a fatal page
99
- * exception.
100
- *
101
- * @license MIT
102
- * @see {@link https://github.com/octet-stream/form-data-encoder/blob/master/lib/util/isFormData.ts}
103
- */
104
- function isFormData(value: any) {
105
- return (
106
- value &&
107
- isFunction(value.constructor) &&
108
- value[Symbol.toStringTag] === 'FormData' &&
109
- isFunction(value.append) &&
110
- isFunction(value.getAll) &&
111
- isFunction(value.entries) &&
112
- isFunction(value[Symbol.iterator])
113
- );
114
- }
115
-
116
- function getFileFromSuppliedFiles(filename: string, files: FetchHAROptions['files']) {
117
- if (filename in files) {
118
- return files[filename];
119
- } else if (decodeURIComponent(filename) in files) {
120
- return files[decodeURIComponent(filename)];
121
- }
122
-
123
- return false;
124
- }
125
-
126
- export default function fetchHAR(har: Har, opts: FetchHAROptions = {}) {
127
- if (!har) throw new Error('Missing HAR definition');
128
- if (!har.log || !har.log.entries || !har.log.entries.length) throw new Error('Missing log.entries array');
129
-
130
- const { request } = har.log.entries[0];
131
- const { url } = request;
132
- let querystring = '';
133
- let shouldSetDuplex = false;
134
-
135
- const options: RequestInitWithDuplex = {
136
- // If we have custom options for the `Request` API we need to add them in here now before we
137
- // fill it in with everything we need from the HAR.
138
- ...(opts.init ? opts.init : {}),
139
- method: request.method,
140
- };
141
-
142
- if (!options.headers) {
143
- options.headers = new Headers();
144
- } else if (typeof options.headers === 'object' && !(options.headers instanceof Headers) && options.headers !== null) {
145
- options.headers = new Headers(options.headers);
146
- }
147
-
148
- const headers = options.headers as Headers;
149
- if ('headers' in request && request.headers.length) {
150
- // eslint-disable-next-line consistent-return
151
- request.headers.forEach(header => {
152
- try {
153
- return headers.append(header.name, header.value);
154
- } catch (err) {
155
- /**
156
- * `Headers.append()` will throw errors if the header name is not a legal HTTP header name,
157
- * like `X-API-KEY (Header)`. If that happens instead of tossing the error back out, we
158
- * should silently just ignore
159
- * it.
160
- */
161
- }
162
- });
163
- }
164
-
165
- if ('cookies' in request && request.cookies.length) {
166
- /**
167
- * As the browser fetch API can't set custom cookies for requests, they instead need to be
168
- * defined on the document and passed into the request via `credentials: include`. Since this
169
- * is a browser-specific quirk, that should only
170
- * happen in browsers!
171
- */
172
- if (isBrowser()) {
173
- request.cookies.forEach(cookie => {
174
- document.cookie = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
175
- });
176
-
177
- options.credentials = 'include';
178
- } else {
179
- headers.append(
180
- 'cookie',
181
- request.cookies
182
- .map(cookie => `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`)
183
- .join('; ')
184
- );
185
- }
186
- }
187
-
188
- if ('postData' in request) {
189
- if ('params' in request.postData) {
190
- if (!('mimeType' in request.postData)) {
191
- // @ts-expect-error HAR spec requires that `mimeType` is always present but it might not be.
192
- request.postData.mimeType = 'application/octet-stream';
193
- }
194
-
195
- switch (request.postData.mimeType) {
196
- case 'application/x-www-form-urlencoded':
197
- /**
198
- * Since the content we're handling here is to be encoded as
199
- * `application/x-www-form-urlencoded`, this should override any other `Content-Type`
200
- * headers that are present in the HAR. This is how Postman handles this case when
201
- * building code snippets!
202
- *
203
- * @see {@link https://github.com/github/fetch/issues/263#issuecomment-209530977}
204
- */
205
- headers.set('Content-Type', request.postData.mimeType);
206
-
207
- const encodedParams = new URLSearchParams();
208
- request.postData.params.forEach(param => encodedParams.set(param.name, param.value));
209
-
210
- options.body = encodedParams.toString();
211
- break;
212
-
213
- case 'multipart/alternative':
214
- case 'multipart/form-data':
215
- case 'multipart/mixed':
216
- case 'multipart/related':
217
- /**
218
- * If there's a `Content-Type` header set we need to remove it. We're doing this because
219
- * when we pass the form data object into `fetch` that'll set a proper `Content-Type`
220
- * header for this request that also includes the boundary used on the content.
221
- *
222
- * If we don't do this, then consumers won't be able to parse out the payload because
223
- * they won't know what the boundary to split on it.
224
- */
225
- if (headers.has('Content-Type')) {
226
- headers.delete('Content-Type');
227
- }
228
-
229
- const form = new FormData();
230
- if (!isFormData(form)) {
231
- /**
232
- * The `form-data` NPM module returns one of two things: a native `FormData` API or its
233
- * own polyfill. Unfortunately this polyfill does not support the full API of the native
234
- * FormData object so when you load `form-data` within a browser environment you'll
235
- * have two major differences in API:
236
- *
237
- * - The `.append()` API in `form-data` requires that the third argument is an object
238
- * containing various, undocumented, options. In the browser, `.append()`'s third
239
- * argument should only be present when the second is a `Blob` or `USVString`, and
240
- * when it is present, it should be a filename string.
241
- * - `form-data` does not expose an `.entries()` API, so the only way to retrieve data
242
- * out of it for construction of boundary-separated payload content is to use its
243
- * `.pipe()` API. Since the browser doesn't have this API, you'll be unable to
244
- * retrieve data out of it.
245
- *
246
- * Now since the native `FormData` API is iterable, and has the `.entries()` iterator,
247
- * we can easily detect if we have a native copy of the FormData API. It's for all of
248
- * these reasons that we're opting to hard crash here because supporting this
249
- * non-compliant API is more trouble than its worth.
250
- *
251
- * @see {@link https://github.com/form-data/form-data/issues/124}
252
- */
253
- throw new Error(
254
- "We've detected you're using a non-spec compliant FormData library. We recommend polyfilling FormData with https://npm.im/formdata-node"
255
- );
256
- }
257
-
258
- request.postData.params.forEach(param => {
259
- if ('fileName' in param) {
260
- if (opts.files) {
261
- const fileContents = getFileFromSuppliedFiles(param.fileName, opts.files);
262
- if (fileContents) {
263
- // If the file we've got available to us is a Buffer then we need to convert it so
264
- // that the FormData API can use it.
265
- if (isBuffer(fileContents)) {
266
- form.append(
267
- param.name,
268
- new File([fileContents], param.fileName, {
269
- type: param.contentType || null,
270
- }),
271
- param.fileName
272
- );
273
-
274
- return;
275
- } else if (isFile(fileContents)) {
276
- form.append(param.name, fileContents as Blob, param.fileName);
277
- return;
278
- }
279
-
280
- throw new TypeError(
281
- 'An unknown object has been supplied into the `files` config for use. We only support instances of the File API and Node Buffer objects.'
282
- );
283
- }
284
- }
285
-
286
- if ('value' in param) {
287
- let paramBlob;
288
- const parsed = parseDataUrl(param.value);
289
- if (parsed) {
290
- // If we were able to parse out this data URL we don't need to transform its data
291
- // into a buffer for `Blob` because that supports data URLs already.
292
- paramBlob = new Blob([param.value], { type: parsed.contentType || param.contentType || null });
293
- } else {
294
- paramBlob = new Blob([param.value], { type: param.contentType || null });
295
- }
296
-
297
- form.append(param.name, paramBlob, param.fileName);
298
- return;
299
- }
300
-
301
- throw new Error(
302
- "The supplied HAR has a postData parameter with `fileName`, but neither `value` content within the HAR or any file buffers were supplied with the `files` option. Since this library doesn't have access to the filesystem, it can't fetch that file."
303
- );
304
- }
305
-
306
- form.append(param.name, param.value);
307
- });
308
-
309
- /**
310
- * If a the `fetch` polyfill that's being used here doesn't have spec-compliant handling
311
- * for the `FormData` API (like `node-fetch@2`), then you should pass in a handler (like
312
- * the `form-data-encoder` library) to transform its contents into something that can be
313
- * used with the `Request` object.
314
- *
315
- * @see {@link https://www.npmjs.com/package/formdata-node}
316
- */
317
- if (opts.multipartEncoder) {
318
- // eslint-disable-next-line new-cap
319
- const encoder = new opts.multipartEncoder(form);
320
- Object.keys(encoder.headers).forEach(header => {
321
- headers.set(header, encoder.headers[header]);
322
- });
323
-
324
- // @ts-expect-error "Property 'from' does not exist on type 'typeof Readable'." but it does!
325
- options.body = Readable.from(encoder);
326
- shouldSetDuplex = true;
327
- } else {
328
- options.body = form;
329
- }
330
- break;
331
-
332
- default:
333
- const formBody: Record<string, unknown> = {};
334
- request.postData.params.map(param => {
335
- try {
336
- formBody[param.name] = JSON.parse(param.value);
337
- } catch (e) {
338
- formBody[param.name] = param.value;
339
- }
340
-
341
- return true;
342
- });
343
-
344
- options.body = JSON.stringify(formBody);
345
- }
346
- } else if (request.postData.text?.length) {
347
- // If we've got `files` map content present, and this post data content contains a valid data
348
- // URL then we can substitute the payload with that file instead of the using data URL.
349
- if (opts.files) {
350
- const parsed = parseDataUrl(request.postData.text) as DataURL;
351
- if (parsed) {
352
- if (parsed?.name && parsed.name in opts.files) {
353
- const fileContents = getFileFromSuppliedFiles(parsed.name, opts.files);
354
- if (fileContents) {
355
- if (isBuffer(fileContents)) {
356
- options.body = fileContents;
357
- } else if (isFile(fileContents)) {
358
- // `Readable.from` isn't available in browsers but the browser `Request` object can
359
- // handle `File` objects just fine without us having to mold it into shape.
360
- if (isBrowser()) {
361
- options.body = fileContents;
362
- } else {
363
- // @ts-expect-error "Property 'from' does not exist on type 'typeof Readable'." but it does!
364
- options.body = Readable.from((fileContents as File).stream());
365
- shouldSetDuplex = true;
366
-
367
- // Supplying a polyfilled `File` stream into `Request.body` doesn't automatically
368
- // add `Content-Length`.
369
- if (!headers.has('content-length')) {
370
- headers.set('content-length', String((fileContents as File).size));
371
- }
372
- }
373
- }
374
- }
375
- }
376
- }
377
- }
378
-
379
- if (typeof options.body === 'undefined') {
380
- options.body = request.postData.text;
381
- }
382
- }
383
-
384
- /**
385
- * The fetch spec, which Node 18+ strictly abides by, now requires that `duplex` be sent with
386
- * requests that have payloads.
387
- *
388
- * As `RequestInit#duplex` isn't supported by any browsers, or even mentioned on MDN, we aren't
389
- * sending it in browser environments. This work is purely to support Node 18+ and `undici`
390
- * environments.
391
- *
392
- * @see {@link https://github.com/nodejs/node/issues/46221}
393
- * @see {@link https://github.com/whatwg/fetch/pull/1457}
394
- * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Request/Request}
395
- */
396
- if (shouldSetDuplex && !isBrowser()) {
397
- options.duplex = 'half';
398
- }
399
- }
400
-
401
- // We automaticaly assume that the HAR that we have already has query parameters encoded within
402
- // it so we do **not** use the `URLSearchParams` API here for composing the query string.
403
- let requestURL = url;
404
- if ('queryString' in request && request.queryString.length) {
405
- const urlObj = new URL(requestURL);
406
-
407
- const queryParams = Array.from(urlObj.searchParams).map(([k, v]) => `${k}=${v}`);
408
- request.queryString.forEach(q => {
409
- queryParams.push(`${q.name}=${q.value}`);
410
- });
411
-
412
- querystring = queryParams.join('&');
413
-
414
- // Because anchor hashes before query strings will prevent query strings from being delivered
415
- // we need to pop them off and re-add them after.
416
- if (urlObj.hash) {
417
- const urlWithoutHashes = requestURL.replace(urlObj.hash, '');
418
- requestURL = `${urlWithoutHashes.split('?')[0]}${querystring ? `?${querystring}` : ''}`;
419
- requestURL += urlObj.hash;
420
- } else {
421
- requestURL = `${requestURL.split('?')[0]}${querystring ? `?${querystring}` : ''}`;
422
- }
423
- }
424
-
425
- if (opts.userAgent) {
426
- headers.append('User-Agent', opts.userAgent);
427
- }
428
-
429
- options.headers = headers;
430
-
431
- return fetch(requestURL, options);
432
- }
package/tsconfig.json DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "allowJs": true,
4
- "baseUrl": "./src",
5
- "declaration": true,
6
- "esModuleInterop": true,
7
- "lib": ["dom", "es2020"],
8
- "noImplicitAny": true,
9
- "outDir": "dist/"
10
- },
11
- "include": ["./src/**/*"]
12
- }