@whatwg-node/fetch
Version:
Cross Platform Smart Fetch Ponyfill
352 lines (313 loc) • 12 kB
JavaScript
const http2 = require('http2')
const handleFileRequest = require("./handle-file-request");
const readableStreamToReadable = require("./readableStreamToReadable");
module.exports = function createNodePonyfill(opts = {}) {
// Bun already has a Fetch API
if (process.versions.bun) {
return globalThis;
}
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.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) {
try {
const streamsWeb = require("stream/web");
ponyfills.ReadableStream = streamsWeb.ReadableStream;
ponyfills.WritableStream = streamsWeb.WritableStream;
ponyfills.TransformStream = streamsWeb.TransformStream;
} catch (e) {
const streamsWeb = require("web-streams-polyfill/ponyfill");
ponyfills.ReadableStream = streamsWeb.ReadableStream;
ponyfills.WritableStream = streamsWeb.WritableStream;
ponyfills.TransformStream = streamsWeb.TransformStream;
}
}
ponyfills.btoa = globalThis.btoa
if (!ponyfills.btoa) {
ponyfills.btoa = function btoa(data) {
return Buffer.from(data, 'binary').toString('base64');
};
}
ponyfills.TextEncoder = function TextEncoder(encoding = 'utf-8') {
return {
encode(str) {
return Buffer.from(str, encoding);
}
}
}
ponyfills.TextDecoder = function TextDecoder(encoding = 'utf-8') {
return {
decode(buf) {
return Buffer.from(buf).toString(encoding);
}
}
}
if (!ponyfills.crypto) {
const cryptoModule = require("crypto");
ponyfills.crypto = cryptoModule.webcrypto;
}
if (!ponyfills.crypto) {
const cryptoPonyfill = require('@peculiar/webcrypto');
ponyfills.crypto = new cryptoPonyfill.Crypto();
}
if (!ponyfills.URLPattern) {
const urlPatternModule = require('urlpattern-polyfill');
ponyfills.URLPattern = urlPatternModule.URLPattern;
}
// 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;
}