@whatwg-node/fetch
Advanced tools
Comparing version 0.6.1 to 0.6.2-alpha-20230102102521-2e7c4e5
# @whatwg-node/fetch | ||
## 0.6.2-alpha-20230102102521-2e7c4e5 | ||
### Patch Changes | ||
- [#154](https://github.com/ardatan/whatwg-node/pull/154) [`02b2895`](https://github.com/ardatan/whatwg-node/commit/02b289550751fd2a8317666ecad101037a4e6551) Thanks [@ardatan](https://github.com/ardatan)! - dependencies updates: | ||
- Updated dependency [`web-streams-polyfill@^3.2.1` ↗︎](https://www.npmjs.com/package/web-streams-polyfill/v/3.2.1) (from `^3.2.0`, in `dependencies`) | ||
- Added dependency [`@whatwg-node/node-fetch@0.0.0` ↗︎](https://www.npmjs.com/package/@whatwg-node/node-fetch/v/0.0.0) (to `dependencies`) | ||
- Removed dependency [`abort-controller@^3.0.0` ↗︎](https://www.npmjs.com/package/abort-controller/v/3.0.0) (from `dependencies`) | ||
- Removed dependency [`form-data-encoder@^1.7.1` ↗︎](https://www.npmjs.com/package/form-data-encoder/v/1.7.1) (from `dependencies`) | ||
- Removed dependency [`formdata-node@^4.3.1` ↗︎](https://www.npmjs.com/package/formdata-node/v/4.3.1) (from `dependencies`) | ||
- Removed dependency [`node-fetch@^2.6.7` ↗︎](https://www.npmjs.com/package/node-fetch/v/2.6.7) (from `dependencies`) | ||
- Removed dependency [`undici@^5.12.0` ↗︎](https://www.npmjs.com/package/undici/v/5.12.0) (from `dependencies`) | ||
- [#154](https://github.com/ardatan/whatwg-node/pull/154) [`02b2895`](https://github.com/ardatan/whatwg-node/commit/02b289550751fd2a8317666ecad101037a4e6551) Thanks [@ardatan](https://github.com/ardatan)! - New Fetch API implementation for Node | ||
- Updated dependencies [[`02b2895`](https://github.com/ardatan/whatwg-node/commit/02b289550751fd2a8317666ecad101037a4e6551)]: | ||
- @whatwg-node/node-fetch@0.0.1-alpha-20230102102521-2e7c4e5 | ||
## 0.6.1 | ||
@@ -4,0 +23,0 @@ |
@@ -1,5 +0,1 @@ | ||
const http2 = require('http2') | ||
const handleFileRequest = require("./handle-file-request"); | ||
const readableStreamToReadable = require("./readableStreamToReadable"); | ||
module.exports = function createNodePonyfill(opts = {}) { | ||
@@ -12,42 +8,19 @@ | ||
const newNodeFetch = require('@whatwg-node/node-fetch'); | ||
const ponyfills = {}; | ||
if (!opts.useNodeFetch) { | ||
ponyfills.fetch = globalThis.fetch; | ||
ponyfills.Headers = globalThis.Headers; | ||
ponyfills.Request = globalThis.Request; | ||
ponyfills.Response = globalThis.Response; | ||
ponyfills.FormData = globalThis.FormData; | ||
ponyfills.File = globalThis.File; | ||
} | ||
ponyfills.AbortController = globalThis.AbortController; | ||
ponyfills.ReadableStream = globalThis.ReadableStream; | ||
ponyfills.fetch = newNodeFetch.fetch; | ||
ponyfills.Request = newNodeFetch.Request; | ||
ponyfills.Response = newNodeFetch.Response; | ||
ponyfills.Headers = newNodeFetch.Headers; | ||
ponyfills.FormData = newNodeFetch.FormData; | ||
ponyfills.AbortController = newNodeFetch.AbortController; | ||
ponyfills.ReadableStream = newNodeFetch.ReadableStream; | ||
ponyfills.WritableStream = globalThis.WritableStream; | ||
ponyfills.TransformStream = globalThis.TransformStream; | ||
ponyfills.Blob = globalThis.Blob; | ||
ponyfills.crypto = globalThis.crypto; | ||
ponyfills.URLPattern = globalThis.URLPattern; | ||
if (!ponyfills.AbortController) { | ||
const abortControllerModule = require("abort-controller"); | ||
ponyfills.AbortController = | ||
abortControllerModule.default || abortControllerModule; | ||
} | ||
if (!ponyfills.Blob) { | ||
const bufferModule = require('buffer') | ||
ponyfills.Blob = bufferModule.Blob; | ||
} | ||
if (!ponyfills.Blob) { | ||
const formDataModule = require("formdata-node"); | ||
ponyfills.Blob = formDataModule.Blob | ||
} | ||
if (!ponyfills.ReadableStream) { | ||
if (!ponyfills.WritableStream) { | ||
try { | ||
const streamsWeb = require("stream/web"); | ||
ponyfills.ReadableStream = streamsWeb.ReadableStream; | ||
ponyfills.WritableStream = streamsWeb.WritableStream; | ||
@@ -57,3 +30,2 @@ ponyfills.TransformStream = streamsWeb.TransformStream; | ||
const streamsWeb = require("web-streams-polyfill/ponyfill"); | ||
ponyfills.ReadableStream = streamsWeb.ReadableStream; | ||
ponyfills.WritableStream = streamsWeb.WritableStream; | ||
@@ -64,23 +36,36 @@ ponyfills.TransformStream = streamsWeb.TransformStream; | ||
ponyfills.btoa = globalThis.btoa | ||
if (!ponyfills.btoa) { | ||
ponyfills.btoa = function btoa(data) { | ||
return Buffer.from(data, 'binary').toString('base64'); | ||
}; | ||
} | ||
ponyfills.Blob = newNodeFetch.Blob; | ||
ponyfills.File = newNodeFetch.File; | ||
ponyfills.crypto = globalThis.crypto; | ||
ponyfills.btoa = newNodeFetch.btoa; | ||
ponyfills.TextEncoder = newNodeFetch.TextEncoder; | ||
ponyfills.TextDecoder = newNodeFetch.TextDecoder; | ||
ponyfills.AbortError = newNodeFetch.AbortError; | ||
ponyfills.AbortSignal = newNodeFetch.AbortSignal; | ||
ponyfills.TextEncoder = function TextEncoder(encoding = 'utf-8') { | ||
return { | ||
encode(str) { | ||
return Buffer.from(str, encoding); | ||
if (opts.formDataLimits) { | ||
ponyfills.Body = class Body extends newNodeFetch.Body { | ||
constructor(body, userOpts) { | ||
super(body, { | ||
formDataLimits: opts.formDataLimits, | ||
...userOpts, | ||
}); | ||
} | ||
} | ||
} | ||
ponyfills.TextDecoder = function TextDecoder(encoding = 'utf-8') { | ||
return { | ||
decode(buf) { | ||
return Buffer.from(buf).toString(encoding); | ||
ponyfills.Request = class Request extends newNodeFetch.Request { | ||
constructor(input, userOpts) { | ||
super(input, { | ||
formDataLimits: opts.formDataLimits, | ||
...userOpts, | ||
}); | ||
} | ||
} | ||
ponyfills.Response = class Response extends newNodeFetch.Response { | ||
constructor(body, userOpts) { | ||
super(body, { | ||
formDataLimits: opts.formDataLimits, | ||
...userOpts, | ||
}); | ||
} | ||
} | ||
} | ||
@@ -97,3 +82,3 @@ | ||
} | ||
if (!ponyfills.URLPattern) { | ||
@@ -104,253 +89,3 @@ const urlPatternModule = require('urlpattern-polyfill'); | ||
// If any of classes of Fetch API is missing, we need to ponyfill them. | ||
if (!ponyfills.fetch || | ||
!ponyfills.Request || | ||
!ponyfills.Headers || | ||
!ponyfills.Response || | ||
!ponyfills.FormData || | ||
!ponyfills.File || | ||
opts.useNodeFetch) { | ||
const [ | ||
nodeMajorStr, | ||
nodeMinorStr | ||
] = process.versions.node.split('.'); | ||
const nodeMajor = parseInt(nodeMajorStr); | ||
const nodeMinor = parseInt(nodeMinorStr); | ||
const getFormDataMethod = require('./getFormDataMethod'); | ||
if (!opts.useNodeFetch && (nodeMajor > 16 || (nodeMajor === 16 && nodeMinor >= 5))) { | ||
const undici = require("undici"); | ||
if (!ponyfills.Headers) { | ||
ponyfills.Headers = undici.Headers; | ||
} | ||
const streams = require("stream"); | ||
const OriginalRequest = ponyfills.Request || undici.Request; | ||
class Request extends OriginalRequest { | ||
constructor(requestOrUrl, options) { | ||
if (typeof requestOrUrl === "string" || requestOrUrl instanceof URL) { | ||
if (options != null && typeof options === "object" && !options.duplex) { | ||
options.duplex = 'half'; | ||
} | ||
super(requestOrUrl, options); | ||
const contentType = this.headers.get("content-type"); | ||
if (contentType && contentType.startsWith("multipart/form-data")) { | ||
this.headers.set("content-type", contentType.split(', ')[0]); | ||
} | ||
} else { | ||
super(requestOrUrl); | ||
} | ||
this.formData = getFormDataMethod(undici.File, opts.formDataLimits); | ||
} | ||
} | ||
ponyfills.Request = Request; | ||
const originalFetch = ponyfills.fetch || undici.fetch; | ||
const fetch = function (requestOrUrl, options) { | ||
if (typeof requestOrUrl === "string" || requestOrUrl instanceof URL) { | ||
if (options != null && typeof options === "object" && !options.duplex) { | ||
options.duplex = 'half'; | ||
} | ||
// We cannot use our ctor because it leaks on Node 18's global fetch | ||
return originalFetch(requestOrUrl, options); | ||
} | ||
if (requestOrUrl.url.startsWith('file:')) { | ||
return handleFileRequest(requestOrUrl.url, ponyfills.Response); | ||
} | ||
return originalFetch(requestOrUrl); | ||
}; | ||
ponyfills.fetch = fetch; | ||
if (!ponyfills.Response) { | ||
ponyfills.Response = undici.Response; | ||
} | ||
if (!ponyfills.FormData) { | ||
ponyfills.FormData = undici.FormData; | ||
} | ||
if (!ponyfills.File) { | ||
ponyfills.File = undici.File | ||
} | ||
} else { | ||
const nodeFetch = require("node-fetch"); | ||
const realFetch = ponyfills.fetch || nodeFetch.default || nodeFetch; | ||
if (!ponyfills.Headers) { | ||
ponyfills.Headers = nodeFetch.Headers; | ||
// Sveltekit | ||
if (globalThis.Headers && nodeMajor < 18) { | ||
Object.defineProperty(globalThis.Headers, Symbol.hasInstance, { | ||
value(obj) { | ||
return obj && obj.get && obj.set && obj.delete && obj.has && obj.append; | ||
}, | ||
configurable: true, | ||
}) | ||
} | ||
} | ||
const formDataEncoderModule = require("form-data-encoder"); | ||
const streams = require("stream"); | ||
const formDataModule = require("formdata-node"); | ||
if (!ponyfills.FormData) { | ||
ponyfills.FormData = formDataModule.FormData | ||
} | ||
if (!ponyfills.File) { | ||
ponyfills.File = formDataModule.File | ||
} | ||
const OriginalRequest = ponyfills.Request || nodeFetch.Request; | ||
class Request extends OriginalRequest { | ||
constructor(requestOrUrl, options) { | ||
if (typeof requestOrUrl === "string" || requestOrUrl instanceof URL) { | ||
// Support schemaless URIs on the server for parity with the browser. | ||
// Ex: //github.com/ -> https://github.com/ | ||
if (/^\/\//.test(requestOrUrl.toString())) { | ||
requestOrUrl = "https:" + requestOrUrl.toString(); | ||
} | ||
let method = (options || {}).method; | ||
const headers = {}; | ||
if ('headers' in (options || {})) { | ||
let isHttp2 = false; | ||
for (const [key, value] of Object.entries(options.headers)) { | ||
if (key.startsWith(':')) { | ||
// omit http2 headers | ||
isHttp2 = true; | ||
} else { | ||
headers[key] = value | ||
} | ||
} | ||
if (isHttp2) { | ||
// translate http2 if applicable | ||
method = options.headers[http2.constants.HTTP2_HEADER_METHOD]; | ||
const scheme = options.headers[http2.constants.HTTP2_HEADER_SCHEME]; | ||
const authority = options.headers[http2.constants.HTTP2_HEADER_AUTHORITY]; | ||
const path = options.headers[http2.constants.HTTP2_HEADER_PATH]; | ||
headers.host = authority; | ||
requestOrUrl = `${scheme}://${authority}${path}` | ||
} | ||
} | ||
const fixedOptions = { | ||
...options, | ||
method, | ||
headers, | ||
}; | ||
fixedOptions.headers = new ponyfills.Headers(fixedOptions.headers || {}); | ||
fixedOptions.headers.set('Connection', 'keep-alive'); | ||
if (fixedOptions.body != null) { | ||
if (fixedOptions.body[Symbol.toStringTag] === 'FormData') { | ||
const encoder = new formDataEncoderModule.FormDataEncoder(fixedOptions.body) | ||
for (const headerKey in encoder.headers) { | ||
fixedOptions.headers.set(headerKey, encoder.headers[headerKey]) | ||
} | ||
fixedOptions.body = streams.Readable.from(encoder); | ||
} else if (fixedOptions.body[Symbol.toStringTag] === 'ReadableStream') { | ||
fixedOptions.body = readableStreamToReadable(fixedOptions.body); | ||
} | ||
} | ||
super(requestOrUrl, fixedOptions); | ||
} else { | ||
super(requestOrUrl); | ||
} | ||
this.formData = getFormDataMethod(formDataModule.File, opts.formDataLimits); | ||
} | ||
} | ||
ponyfills.Request = Request; | ||
const fetch = function (requestOrUrl, options) { | ||
if (typeof requestOrUrl === "string" || requestOrUrl instanceof URL) { | ||
return fetch(new Request(requestOrUrl, options)); | ||
} | ||
if (requestOrUrl.url.startsWith('file:')) { | ||
return handleFileRequest(requestOrUrl.url, ponyfills.Response); | ||
} | ||
const abortCtrl = new ponyfills.AbortController(); | ||
return realFetch(requestOrUrl, { | ||
...options, | ||
signal: abortCtrl.signal | ||
}).then(res => { | ||
return new Proxy(res, { | ||
get(target, prop, receiver) { | ||
if (prop === 'body') { | ||
return new Proxy(res.body, { | ||
get(target, prop, receiver) { | ||
if (prop === Symbol.asyncIterator) { | ||
return () => { | ||
const originalAsyncIterator = target[Symbol.asyncIterator](); | ||
return { | ||
next() { | ||
return originalAsyncIterator.next(); | ||
}, | ||
return() { | ||
abortCtrl.abort(); | ||
return originalAsyncIterator.return(); | ||
}, | ||
throw(error) { | ||
abortCtrl.abort(error); | ||
return originalAsyncIterator.throw(error); | ||
} | ||
} | ||
} | ||
} | ||
return Reflect.get(target, prop, receiver); | ||
} | ||
}) | ||
} | ||
return Reflect.get(target, prop, receiver); | ||
} | ||
}) | ||
}); | ||
}; | ||
ponyfills.fetch = fetch; | ||
const OriginalResponse = ponyfills.Response || nodeFetch.Response; | ||
ponyfills.Response = function Response(body, init) { | ||
if (body != null && body[Symbol.toStringTag] === 'ReadableStream') { | ||
const actualBody = readableStreamToReadable(body); | ||
// Polyfill ReadableStream is not working well with node-fetch's Response | ||
return new OriginalResponse(actualBody, init); | ||
} | ||
return new OriginalResponse(body, init); | ||
}; | ||
} | ||
} | ||
if (!ponyfills.Response.redirect) { | ||
ponyfills.Response.redirect = function (url, status = 302) { | ||
return new ponyfills.Response(null, { | ||
status, | ||
headers: { | ||
Location: url, | ||
}, | ||
}); | ||
}; | ||
} | ||
if (!ponyfills.Response.json) { | ||
ponyfills.Response.json = function (data, init = {}) { | ||
return new ponyfills.Response(JSON.stringify(data), { | ||
...init, | ||
headers: { | ||
"Content-Type": "application/json", | ||
...init.headers, | ||
}, | ||
}); | ||
}; | ||
} | ||
if (!ponyfills.Response.error) { | ||
ponyfills.Response.error = function () { | ||
return new ponyfills.Response(null, { | ||
status: 500, | ||
}); | ||
}; | ||
} | ||
return ponyfills; | ||
} |
{ | ||
"name": "@whatwg-node/fetch", | ||
"version": "0.6.1", | ||
"version": "0.6.2-alpha-20230102102521-2e7c4e5", | ||
"description": "Cross Platform Smart Fetch Ponyfill", | ||
@@ -22,10 +22,6 @@ "author": "Arda TANRIKULU <ardatanrikulu@gmail.com>", | ||
"@peculiar/webcrypto": "^1.4.0", | ||
"abort-controller": "^3.0.0", | ||
"@whatwg-node/node-fetch": "0.0.1-alpha-20230102102521-2e7c4e5", | ||
"busboy": "^1.6.0", | ||
"form-data-encoder": "^1.7.1", | ||
"formdata-node": "^4.3.1", | ||
"node-fetch": "^2.6.7", | ||
"undici": "^5.12.0", | ||
"web-streams-polyfill": "^3.2.0", | ||
"urlpattern-polyfill": "^6.0.2" | ||
"urlpattern-polyfill": "^6.0.2", | ||
"web-streams-polyfill": "^3.2.1" | ||
}, | ||
@@ -32,0 +28,0 @@ "publishConfig": { |
@@ -1,36 +0,58 @@ | ||
import { createTestContainer } from '../../server/test/create-test-container'; | ||
import * as fetchAPI from '@whatwg-node/fetch'; | ||
describe('getFormDataMethod', () => { | ||
createTestContainer(fetchAPI => { | ||
it('should parse fields correctly', async () => { | ||
const formData = new fetchAPI.FormData(); | ||
formData.append('greetings', 'Hello world!'); | ||
formData.append('bye', 'Goodbye world!'); | ||
const request = new fetchAPI.Request('http://localhost:8080', { | ||
method: 'POST', | ||
body: formData, | ||
}); | ||
const formdata = await request.formData(); | ||
expect(formdata.get('greetings')).toBe('Hello world!'); | ||
expect(formdata.get('bye')).toBe('Goodbye world!'); | ||
it('should parse fields correctly', async () => { | ||
const formData = new fetchAPI.FormData(); | ||
formData.append('greetings', 'Hello world!'); | ||
formData.append('bye', 'Goodbye world!'); | ||
const request = new fetchAPI.Request('http://localhost:8080', { | ||
method: 'POST', | ||
body: formData, | ||
}); | ||
it('should parse and receive text files correctly', async () => { | ||
const formData = new fetchAPI.FormData(); | ||
const greetingsFile = new fetchAPI.File(['Hello world!'], 'greetings.txt', { type: 'text/plain' }); | ||
const byeFile = new fetchAPI.File(['Goodbye world!'], 'bye.txt', { type: 'text/plain' }); | ||
formData.append('greetings', greetingsFile); | ||
formData.append('bye', byeFile); | ||
const request = new fetchAPI.Request('http://localhost:8080', { | ||
method: 'POST', | ||
body: formData, | ||
}); | ||
const formdata = await request.formData(); | ||
const receivedGreetingsFile = formdata.get('greetings') as File; | ||
const receivedGreetingsText = await receivedGreetingsFile.text(); | ||
expect(receivedGreetingsText).toBe('Hello world!'); | ||
const receivedByeFile = formdata.get('bye') as File; | ||
const receivedByeText = await receivedByeFile.text(); | ||
expect(receivedByeText).toBe('Goodbye world!'); | ||
const formdata = await request.formData(); | ||
expect(formdata.get('greetings')).toBe('Hello world!'); | ||
expect(formdata.get('bye')).toBe('Goodbye world!'); | ||
}); | ||
it('should parse and receive text files correctly', async () => { | ||
const formData = new fetchAPI.FormData(); | ||
const greetingsFile = new fetchAPI.File(['Hello world!'], 'greetings.txt', { type: 'text/plain' }); | ||
const byeFile = new fetchAPI.File(['Goodbye world!'], 'bye.txt', { type: 'text/plain' }); | ||
formData.append('greetings', greetingsFile); | ||
formData.append('bye', byeFile); | ||
const request = new fetchAPI.Request('http://localhost:8080', { | ||
method: 'POST', | ||
body: formData, | ||
}); | ||
const formdata = await request.formData(); | ||
const receivedGreetingsFile = formdata.get('greetings') as File; | ||
const receivedGreetingsText = await receivedGreetingsFile.text(); | ||
expect(receivedGreetingsText).toBe('Hello world!'); | ||
const receivedByeFile = formdata.get('bye') as File; | ||
const receivedByeText = await receivedByeFile.text(); | ||
expect(receivedByeText).toBe('Goodbye world!'); | ||
}); | ||
it('should handle file limits', async () => { | ||
const limitedFetchAPI = fetchAPI.createFetch({ | ||
formDataLimits: { | ||
fileSize: 1, | ||
}, | ||
}); | ||
const formData = new limitedFetchAPI.FormData(); | ||
const greetingsFile = new limitedFetchAPI.File(['Hello world!'], 'greetings.txt', { type: 'text/plain' }); | ||
formData.append('greetings', greetingsFile); | ||
const proxyRequest = new limitedFetchAPI.Request('http://localhost:8080', { | ||
method: 'POST', | ||
body: formData, | ||
}); | ||
const formDataInText = await proxyRequest.text(); | ||
const contentType = proxyRequest.headers.get('content-type')!; | ||
const requestWillParse = new limitedFetchAPI.Request('http://localhost:8080', { | ||
method: 'POST', | ||
body: formDataInText, | ||
headers: { | ||
'content-type': contentType, | ||
}, | ||
}); | ||
await expect(() => requestWillParse.formData()).rejects.toThrowError('File size limit exceeded: 1 bytes'); | ||
}); | ||
}); |
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
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
5
0
1
19812
9
277
+ Added@whatwg-node/node-fetch@0.0.1-alpha-20230102102521-2e7c4e5
+ Added@types/node@18.19.68(transitive)
+ Added@whatwg-node/events@0.0.2(transitive)
+ Added@whatwg-node/node-fetch@0.0.1-alpha-20230102102521-2e7c4e5(transitive)
+ Addedundici-types@5.26.5(transitive)
- Removedabort-controller@^3.0.0
- Removedform-data-encoder@^1.7.1
- Removedformdata-node@^4.3.1
- Removednode-fetch@^2.6.7
- Removedundici@^5.12.0
- Removed@fastify/busboy@2.1.1(transitive)
- Removedabort-controller@3.0.0(transitive)
- Removedevent-target-shim@5.0.1(transitive)
- Removedform-data-encoder@1.9.0(transitive)
- Removedformdata-node@4.4.1(transitive)
- Removednode-domexception@1.0.0(transitive)
- Removednode-fetch@2.7.0(transitive)
- Removedtr46@0.0.3(transitive)
- Removedundici@5.28.4(transitive)
- Removedweb-streams-polyfill@4.0.0-beta.3(transitive)
- Removedwebidl-conversions@3.0.1(transitive)
- Removedwhatwg-url@5.0.0(transitive)
Updatedweb-streams-polyfill@^3.2.1