fetch-har
Advanced tools
Comparing version 5.0.5 to 6.0.0
/* eslint-disable import/no-extraneous-dependencies, no-console */ | ||
require('isomorphic-fetch'); | ||
const fetchHar = require('.'); | ||
// If executing from an environment without `fetch`, you'll need to polyfill | ||
global.fetch = require('node-fetch'); | ||
global.Headers = require('node-fetch').Headers; | ||
global.Request = require('node-fetch').Request; | ||
global.FormData = require('form-data'); | ||
// If executing from an environment that dodoesn't normally provide fetch() you'll need to polyfill some APIs in order | ||
// to make `multipart/form-data` requests. | ||
if (!globalThis.FormData) { | ||
globalThis.Blob = require('formdata-node').Blob; | ||
globalThis.File = require('formdata-node').File; | ||
globalThis.FormData = require('formdata-node').FormData; | ||
} | ||
@@ -42,3 +45,3 @@ const har = { | ||
fetchHar(har) | ||
.then(request => request.json()) | ||
.then(res => res.json()) | ||
.then(console.log); |
248
index.js
@@ -1,5 +0,74 @@ | ||
/* eslint-disable no-case-declarations */ | ||
const { Readable } = require('readable-stream'); | ||
const parseDataUrl = require('parse-data-url'); | ||
function constructRequest(har, userAgent = false) { | ||
if (!globalThis.Blob) { | ||
try { | ||
// eslint-disable-next-line import/no-extraneous-dependencies | ||
globalThis.Blob = require('formdata-node').Blob; | ||
} catch (e) { | ||
throw new Error( | ||
'Since you do not have the Blob API available in this environment you must install the optional `formdata-node` dependency.' | ||
); | ||
} | ||
} | ||
if (!globalThis.File) { | ||
try { | ||
// eslint-disable-next-line import/no-extraneous-dependencies | ||
globalThis.File = require('formdata-node').File; | ||
} catch (e) { | ||
throw new Error( | ||
'Since you do not have the File API available in this environment you must install the optional `formdata-node` dependency.' | ||
); | ||
} | ||
} | ||
function isBrowser() { | ||
return typeof window !== 'undefined' && typeof document !== 'undefined'; | ||
} | ||
function isBuffer(value) { | ||
return typeof Buffer !== 'undefined' && Buffer.isBuffer(value); | ||
} | ||
function isFile(value) { | ||
if (value instanceof File) { | ||
// The `Blob` polyfill on Node comes back as being an instanceof `File`. Because passing a Blob into | ||
// a File will end up with a corrupted file we want to prevent this. | ||
// | ||
// This object identity crisis does not happen in the browser. | ||
return value.constructor.name === 'File'; | ||
} | ||
return false; | ||
} | ||
/** | ||
* @license MIT | ||
* @see {@link https://github.com/octet-stream/form-data-encoder/blob/master/lib/util/isFunction.ts} | ||
*/ | ||
function isFunction(value) { | ||
return typeof value === 'function'; | ||
} | ||
/** | ||
* We're loading this library in here instead of loading it from `form-data-encoder` because that uses lookbehind | ||
* regex in its main encoder that Safari doesn't support so it throws a fatal page exception. | ||
* | ||
* @license MIT | ||
* @see {@link https://github.com/octet-stream/form-data-encoder/blob/master/lib/util/isFormData.ts} | ||
*/ | ||
function isFormData(value) { | ||
return ( | ||
value && | ||
isFunction(value.constructor) && | ||
value[Symbol.toStringTag] === 'FormData' && // eslint-disable-line compat/compat | ||
isFunction(value.append) && | ||
isFunction(value.getAll) && | ||
isFunction(value.entries) && | ||
isFunction(value[Symbol.iterator]) // eslint-disable-line compat/compat | ||
); | ||
} | ||
function constructRequest(har, opts = { userAgent: false, files: false, multipartEncoder: false }) { | ||
if (!har) throw new Error('Missing HAR definition'); | ||
@@ -34,3 +103,3 @@ if (!har.log || !har.log.entries || !har.log.entries.length) throw new Error('Missing log.entries array'); | ||
// happen in browsers! | ||
if (typeof window !== 'undefined' && typeof document !== 'undefined') { | ||
if (isBrowser()) { | ||
request.cookies.forEach(cookie => { | ||
@@ -72,3 +141,6 @@ document.cookie = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`; | ||
case 'multipart/alternative': | ||
case 'multipart/form-data': | ||
case 'multipart/mixed': | ||
case 'multipart/related': | ||
// If there's a Content-Type header set remove it. We're doing this because when we pass the form data object | ||
@@ -84,55 +156,89 @@ // into `fetch` that'll set a proper `Content-Type` header for this request that also includes the boundary | ||
// The `form-data` NPM module returns one of two things: a native `FormData` API, or its own polyfill. Since | ||
// the polyfill does not support the full API of the native FormData object, when you load `form-data` within | ||
// a browser environment you'll have two major differences in API: | ||
// | ||
// * The `.append()` API in `form-data` requires that the third argument is an object containing various, | ||
// undocumented, options. In the browser, `.append()`'s third argument should only be present when the | ||
// second is a `Blob` or `USVString`, and when it is present, it should be a filename string. | ||
// * `form-data` does not expose an `.entries()` API, so the only way to retrieve data out of it for | ||
// construction of boundary-separated payload content is to use its `.pipe()` API. Since the browser | ||
// doesn't have this API, you'll be unable to retrieve data out of it. | ||
// | ||
// Now since the native `FormData` API is iterable, and has the `.entries()` iterator, we can easily detect | ||
// what version of the FormData API we have access to by looking for this and constructing a simple wrapper | ||
// to disconnect some of this logic so you can work against a single, consistent API. | ||
// | ||
// Having to do this isn't fun, but it's the only way you can write code to work with `multipart/form-data` | ||
// content under a server and browser. | ||
const form = new FormData(); | ||
const isNativeFormData = typeof form[Symbol.iterator] === 'function'; | ||
if (!isFormData(form)) { | ||
// The `form-data` NPM module returns one of two things: a native `FormData` API or its own polyfill. | ||
// Unfortunately this polyfill does not support the full API of the native FormData object so when you load | ||
// `form-data` within a browser environment you'll have two major differences in API: | ||
// | ||
// * The `.append()` API in `form-data` requires that the third argument is an object containing various, | ||
// undocumented, options. In the browser, `.append()`'s third argument should only be present when the | ||
// second is a `Blob` or `USVString`, and when it is present, it should be a filename string. | ||
// * `form-data` does not expose an `.entries()` API, so the only way to retrieve data out of it for | ||
// construction of boundary-separated payload content is to use its `.pipe()` API. Since the browser | ||
// doesn't have this API, you'll be unable to retrieve data out of it. | ||
// | ||
// Now since the native `FormData` API is iterable, and has the `.entries()` iterator, we can easily detect | ||
// if we have a native copy of the FormData API. It's for all of these reasons that we're opting to hard | ||
// crash here because supporting this non-compliant API is more trouble than its worth. | ||
// | ||
// https://github.com/form-data/form-data/issues/124 | ||
throw new Error( | ||
"We've detected you're using a non-spec compliant FormData library. We recommend polyfilling FormData with https://npm.im/formdata-node" | ||
); | ||
} | ||
request.postData.params.forEach(param => { | ||
if ('fileName' in param && !('value' in param)) { | ||
throw new Error( | ||
"The supplied HAR has a postData parameter with `fileName`, but no `value` content. Since this library doesn't have access to the filesystem, it can't fetch that file." | ||
); | ||
} | ||
// If the incoming parameter is a file, and that files value is a data URL, we should decode that and set | ||
// the contents of the value in the HAR to the actual contents of the file. | ||
if ('fileName' in param) { | ||
const parsed = parseDataUrl(param.value); | ||
if (parsed) { | ||
// eslint-disable-next-line no-param-reassign | ||
param.value = parsed.toBuffer().toString(); | ||
} | ||
} | ||
if (opts.files && param.fileName in opts.files) { | ||
const fileContents = opts.files[param.fileName]; | ||
if (isNativeFormData) { | ||
if ('fileName' in param) { | ||
const paramBlob = new Blob([param.value], { type: param.contentType || null }); | ||
// If the file we've got available to us is a Buffer then we need to convert it so that the FormData | ||
// API can use it. | ||
if (isBuffer(fileContents)) { | ||
form.set( | ||
param.name, | ||
new File([fileContents], param.fileName, { | ||
type: param.contentType || null, | ||
}), | ||
param.fileName | ||
); | ||
return; | ||
} else if (isFile(fileContents)) { | ||
form.set(param.name, fileContents, param.fileName); | ||
return; | ||
} | ||
throw new TypeError( | ||
'An unknown object has been supplied into the `files` config for use. We only support instances of the File API and Node Buffer objects.' | ||
); | ||
} else if ('value' in param) { | ||
let paramBlob; | ||
const parsed = parseDataUrl(param.value); | ||
if (parsed) { | ||
// If we were able to parse out this data URL we don't need to transform its data into a buffer for | ||
// `Blob` because that supports data URLs already. | ||
paramBlob = new Blob([param.value], { type: parsed.contentType || param.contentType || null }); | ||
} else { | ||
paramBlob = new Blob([param.value], { type: param.contentType || null }); | ||
} | ||
form.append(param.name, paramBlob, param.fileName); | ||
} else { | ||
form.append(param.name, param.value); | ||
return; | ||
} | ||
} else { | ||
form.append(param.name, param.value || '', { | ||
filename: param.fileName || null, | ||
contentType: param.contentType || null, | ||
}); | ||
throw new Error( | ||
"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." | ||
); | ||
} | ||
form.append(param.name, param.value); | ||
}); | ||
options.body = form; | ||
// If a the `fetch` polyfill that's being used here doesn't have spec-compliant handling for the `FormData` | ||
// API (like `node-fetch@2`), then you should pass in a handler (like the `form-data-encoder` library) to | ||
// transform its contents into something that can be used with the `Request` object. | ||
// | ||
// https://www.npmjs.com/package/formdata-node | ||
if (opts.multipartEncoder) { | ||
// eslint-disable-next-line new-cap | ||
const encoder = new opts.multipartEncoder(form); | ||
Object.keys(encoder.headers).forEach(header => { | ||
headers.set(header, encoder.headers[header]); | ||
}); | ||
options.body = Readable.from(encoder); | ||
} else { | ||
options.body = form; | ||
} | ||
break; | ||
@@ -154,4 +260,31 @@ | ||
} | ||
} else { | ||
options.body = request.postData.text; | ||
} else if (request.postData.text.length) { | ||
// If we've got `files` map content present, and this post data content contains a valid data URL then we can | ||
// substitute the payload with that file instead of the using data URL. | ||
if (opts.files) { | ||
const parsed = parseDataUrl(request.postData.text); | ||
if (parsed && 'name' in parsed && parsed.name in opts.files) { | ||
const fileContents = opts.files[parsed.name]; | ||
if (isBuffer(fileContents)) { | ||
options.body = fileContents; | ||
} else if (isFile(fileContents)) { | ||
// `Readable.from` isn't available in browsers but the browser `Request` object can handle `File` objects | ||
// just fine without us having to mold it into shape. | ||
if (isBrowser()) { | ||
options.body = fileContents; | ||
} else { | ||
options.body = Readable.from(fileContents.stream()); | ||
// Supplying a polyfilled `File` stream into `Request.body` doesn't automatically add `Content-Length`. | ||
if (!headers.has('content-length')) { | ||
headers.set('content-length', fileContents.size); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
if (typeof options.body === 'undefined') { | ||
options.body = request.postData.text; | ||
} | ||
} | ||
@@ -161,8 +294,13 @@ } | ||
if ('queryString' in request && request.queryString.length) { | ||
const query = request.queryString.map(q => `${q.name}=${q.value}`).join('&'); | ||
querystring = `?${query}`; | ||
const queryParams = new URL(url).searchParams; | ||
request.queryString.forEach(q => { | ||
queryParams.append(q.name, q.value); | ||
}); | ||
querystring = queryParams.toString(); | ||
} | ||
if (userAgent) { | ||
headers.append('User-Agent', userAgent); | ||
if (opts.userAgent) { | ||
headers.append('User-Agent', opts.userAgent); | ||
} | ||
@@ -172,7 +310,7 @@ | ||
return new Request(`${url}${querystring}`, options); | ||
return new Request(`${url.split('?')[0]}${querystring ? `?${querystring}` : ''}`, options); | ||
} | ||
function fetchHar(har, userAgent) { | ||
return fetch(constructRequest(har, userAgent)); | ||
function fetchHar(har, opts = { userAgent: false, files: false, multipartEncoder: false }) { | ||
return fetch(constructRequest(har, opts)); | ||
} | ||
@@ -179,0 +317,0 @@ |
{ | ||
"name": "fetch-har", | ||
"version": "5.0.5", | ||
"version": "6.0.0", | ||
"description": "Make a fetch request from a HAR definition", | ||
@@ -15,3 +15,6 @@ "main": "index.js", | ||
"serve": "node __tests__/server.js", | ||
"test": "jest --coverage" | ||
"test:browser": "karma start --single-run", | ||
"test:browser:chrome": "karma start --browsers=Chrome --single-run=false", | ||
"test:browser:debug": "karma start --single-run=false", | ||
"test": "nyc mocha" | ||
}, | ||
@@ -28,21 +31,25 @@ "repository": { | ||
"dependencies": { | ||
"parse-data-url": "^4.0.1" | ||
"parse-data-url": "^4.0.1", | ||
"readable-stream": "^3.6.0" | ||
}, | ||
"optionalDependencies": { | ||
"fromdata-node": "^4.3.2" | ||
}, | ||
"devDependencies": { | ||
"@readme/eslint-config": "^8.0.2", | ||
"body-parser": "^1.19.0", | ||
"cookie-parser": "^1.4.5", | ||
"eslint": "^8.2.0", | ||
"eslint-plugin-compat": "^4.0.0", | ||
"express": "^4.17.1", | ||
"@jsdevtools/host-environment": "^2.1.2", | ||
"@jsdevtools/karma-config": "^3.1.7", | ||
"@readme/eslint-config": "^8.1.2", | ||
"chai": "^4.3.4", | ||
"eslint": "^8.7.0", | ||
"eslint-plugin-compat": "^4.0.1", | ||
"eslint-plugin-mocha": "^10.0.3", | ||
"form-data": "^4.0.0", | ||
"har-examples": "^2.0.1", | ||
"jest": "^27.2.0", | ||
"jest-puppeteer": "^6.0.0", | ||
"multer": "^1.4.2", | ||
"nock": "^13.1.1", | ||
"form-data-encoder": "^1.7.1", | ||
"formdata-node": "^4.3.2", | ||
"har-examples": "^3.0.0", | ||
"isomorphic-fetch": "^3.0.0", | ||
"mocha": "^9.1.4", | ||
"node-fetch": "^2.6.0", | ||
"prettier": "^2.4.1", | ||
"webpack": "^5.53.0", | ||
"webpack-dev-middleware": "^5.1.0" | ||
"nyc": "^15.1.0", | ||
"prettier": "^2.5.1" | ||
}, | ||
@@ -52,12 +59,3 @@ "browserslist": [ | ||
], | ||
"prettier": "@readme/eslint-config/prettier", | ||
"jest": { | ||
"preset": "jest-puppeteer", | ||
"globals": { | ||
"SERVER_URL": "http://localhost:4444" | ||
}, | ||
"testMatch": [ | ||
"<rootDir>/__tests__/*.test.js" | ||
] | ||
} | ||
"prettier": "@readme/eslint-config/prettier" | ||
} |
@@ -16,9 +16,13 @@ # fetch-har | ||
```js | ||
const fetchHar = require('fetch-har'); | ||
require('isomorphic-fetch'); | ||
const fetchHar = require('.'); | ||
// If executing from an environment without `fetch`, you'll need to polyfill. | ||
global.fetch = require('node-fetch'); | ||
global.Headers = require('node-fetch').Headers; | ||
global.Request = require('node-fetch').Request; | ||
global.FormData = require('form-data'); | ||
// If executing from an environment that dodoesn't normally provide fetch() | ||
// you'll need to polyfill some APIs in order to make `multipart/form-data` | ||
// requests. | ||
if (!globalThis.FormData) { | ||
globalThis.Blob = require('formdata-node').Blob; | ||
globalThis.File = require('formdata-node').File; | ||
globalThis.FormData = require('formdata-node').FormData; | ||
} | ||
@@ -40,3 +44,6 @@ const har = { | ||
], | ||
queryString: [{ name: 'a', value: 1 }, { name: 'b', value: 2 }], | ||
queryString: [ | ||
{ name: 'a', value: 1 }, | ||
{ name: 'b', value: 2 }, | ||
], | ||
postData: { | ||
@@ -55,20 +62,46 @@ mimeType: 'application/json', | ||
fetchHar(har) | ||
.then(request => request.json()) | ||
.then(res => res.json()) | ||
.then(console.log); | ||
``` | ||
### `fetchHar(har, userAgent) => Promise` | ||
### API | ||
If you are executing `fetch-har` in a browser environment that supports the [FormData API](https://developer.mozilla.org/en-US/docs/Web/API/FormData) then you don't need to do anything. If you arent, however, you'll need to polyfill it. | ||
- `har` is a [har](https://en.wikipedia.org/wiki/.har) file format. | ||
- `userAgent` is an optional user agent string to let you declare where the request is coming from. | ||
Unfortunately the most popular NPM package [form-data](https://npm.im/form-data) ships with a [non-spec compliant API](https://github.com/form-data/form-data/issues/124), and for this we don't recommend you use it, as if you use `fetch-har` to upload files it may not work. | ||
Performs a fetch request from a given HAR definition. HAR definitions can be used to list lots of requests but we only use the first from the `log.entries` array. | ||
We recommend either [formdata-node](https://npm.im/formdata-node) or [formdata-polyfill](https://npm.im/formdata-polyfill). | ||
### `fetchHar.constructRequest(har, userAgent) => Request` | ||
#### Options | ||
##### userAgent | ||
A custom `User-Agent` header to apply to your request. Please note that browsers have their own handling for these headers in `fetch()` calls so it may not work everywhere; it will always be sent in Node however. | ||
- `har` is a [har](https://en.wikipedia.org/wiki/.har) file format. | ||
- `userAgent` is an optional user agent string to let you declare where the request is coming from. | ||
```js | ||
await fetchHar(har, { userAgent: 'my-client/1.0' }); | ||
``` | ||
We also export a second function which is used to construct a [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object from your HAR. | ||
##### files | ||
An optional object map you can supply to use for `multipart/form-data` file uploads in leu of relying on if the HAR you have has [data URLs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs). It supports Node file buffers and the [File](https://developer.mozilla.org/en-US/docs/Web/API/File) API. | ||
This function is mainly exported for testing purposes but could be useful if you want to construct a request but do not want to execute it right away. | ||
```js | ||
await fetchHar(har, { files: { | ||
'owlbert.png': await fs.readFile('./owlbert.png'), | ||
'file.txt': document.querySelector('#some-file-input').files[0], | ||
} }); | ||
``` | ||
If you don't supply this option `fetch-har` will fallback to the data URL present within the supplied HAR. If no `files` option is present, and no data URL (via `param.value`) is present in the HAR, a fatal exception will be thrown. | ||
##### multipartEncoder | ||
> ❗ If you are using `fetch-har` in Node you may need this option! | ||
If you are running `fetch-har` within a Node environment and you're using `node-fetch@2`, or another `fetch` polyfill that does not support a spec-compliant `FormData` API, you will need to specify an encoder that will transform your `FormData` object into something that can be used with [Request.body](https://developer.mozilla.org/en-US/docs/Web/API/Request/body). | ||
We recommend [form-data-encoder](https://npm.im/form-data-encoder). | ||
```js | ||
const { FormDataEncoder } = require('form-data-encoder'); | ||
await fetchHar(har, { multipartEncoder: FormDataEncoder }); | ||
``` | ||
You do **not**, and shouldn't, need to use this option in browser environments. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
67103
314
105
3
8
2
+ Addedreadable-stream@^3.6.0
+ Addedinherits@2.0.4(transitive)
+ Addedreadable-stream@3.6.2(transitive)
+ Addedsafe-buffer@5.2.1(transitive)
+ Addedstring_decoder@1.3.0(transitive)
+ Addedutil-deprecate@1.0.2(transitive)