2
0
mirror of https://github.com/tenrok/axios.git synced 2026-06-20 20:00:40 +03:00

feat(fetch): add fetch, Request, Response env config variables for the adapter; (#7003)

* feat(fetch): add fetch, Request, Response env config variables for the adapter;

* feat(fetch): fixed design issue for environments without fetch API globals;
This commit is contained in:
Dmitriy Mozgovoy
2025-08-30 22:02:24 +03:00
committed by GitHub
parent a9f47afbf3
commit c959ff2901
7 changed files with 340 additions and 192 deletions
+3
View File
@@ -423,6 +423,9 @@ declare namespace axios {
insecureHTTPParser?: boolean; insecureHTTPParser?: boolean;
env?: { env?: {
FormData?: new (...args: any[]) => object; FormData?: new (...args: any[]) => object;
fetch?: (input: URL | Request | string, init?: RequestInit) => Promise<Response>;
Request?: new (input: (RequestInfo | URL), init?: RequestInit) => Request;
Response?: new (body?: (BodyInit | null), init?: ResponseInit) => Response;
}; };
formSerializer?: FormSerializerOptions; formSerializer?: FormSerializerOptions;
family?: AddressFamily; family?: AddressFamily;
Vendored
+3
View File
@@ -355,6 +355,9 @@ export interface AxiosRequestConfig<D = any> {
insecureHTTPParser?: boolean; insecureHTTPParser?: boolean;
env?: { env?: {
FormData?: new (...args: any[]) => object; FormData?: new (...args: any[]) => object;
fetch?: (input: URL | Request | string, init?: RequestInit) => Promise<Response>;
Request?: new (input: (RequestInfo | URL), init?: RequestInit) => Request;
Response?: new (body?: (BodyInit | null), init?: ResponseInit) => Response;
}; };
formSerializer?: FormSerializerOptions; formSerializer?: FormSerializerOptions;
family?: AddressFamily; family?: AddressFamily;
+6 -4
View File
@@ -1,13 +1,15 @@
import utils from '../utils.js'; import utils from '../utils.js';
import httpAdapter from './http.js'; import httpAdapter from './http.js';
import xhrAdapter from './xhr.js'; import xhrAdapter from './xhr.js';
import fetchAdapter from './fetch.js'; import * as fetchAdapter from './fetch.js';
import AxiosError from "../core/AxiosError.js"; import AxiosError from "../core/AxiosError.js";
const knownAdapters = { const knownAdapters = {
http: httpAdapter, http: httpAdapter,
xhr: xhrAdapter, xhr: xhrAdapter,
fetch: fetchAdapter fetch: {
get: fetchAdapter.getFetch,
}
} }
utils.forEach(knownAdapters, (fn, value) => { utils.forEach(knownAdapters, (fn, value) => {
@@ -26,7 +28,7 @@ const renderReason = (reason) => `- ${reason}`;
const isResolvedHandle = (adapter) => utils.isFunction(adapter) || adapter === null || adapter === false; const isResolvedHandle = (adapter) => utils.isFunction(adapter) || adapter === null || adapter === false;
export default { export default {
getAdapter: (adapters) => { getAdapter: (adapters, config) => {
adapters = utils.isArray(adapters) ? adapters : [adapters]; adapters = utils.isArray(adapters) ? adapters : [adapters];
const {length} = adapters; const {length} = adapters;
@@ -49,7 +51,7 @@ export default {
} }
} }
if (adapter) { if (adapter && (utils.isFunction(adapter) || (adapter = adapter.get(config)))) {
break; break;
} }
+92 -35
View File
@@ -8,14 +8,18 @@ import {progressEventReducer, progressEventDecorator, asyncDecorator} from "../h
import resolveConfig from "../helpers/resolveConfig.js"; import resolveConfig from "../helpers/resolveConfig.js";
import settle from "../core/settle.js"; import settle from "../core/settle.js";
const isFetchSupported = typeof fetch === 'function' && typeof Request === 'function' && typeof Response === 'function'; const DEFAULT_CHUNK_SIZE = 64 * 1024;
const isReadableStreamSupported = isFetchSupported && typeof ReadableStream === 'function';
const {isFunction} = utils;
const globalFetchAPI = (({fetch, Request, Response}) => ({
fetch, Request, Response
}))(utils.global);
const {
ReadableStream, TextEncoder
} = utils.global;
// used only inside the fetch adapter
const encodeText = isFetchSupported && (typeof TextEncoder === 'function' ?
((encoder) => (str) => encoder.encode(str))(new TextEncoder()) :
async (str) => new Uint8Array(await new Response(str).arrayBuffer())
);
const test = (fn, ...args) => { const test = (fn, ...args) => {
try { try {
@@ -25,7 +29,24 @@ const test = (fn, ...args) => {
} }
} }
const supportsRequestStream = isReadableStreamSupported && test(() => { const factory = (env) => {
const {fetch, Request, Response} = Object.assign({}, globalFetchAPI, env);
const isFetchSupported = isFunction(fetch);
const isRequestSupported = isFunction(Request);
const isResponseSupported = isFunction(Response);
if (!isFetchSupported) {
return false;
}
const isReadableStreamSupported = isFetchSupported && isFunction(ReadableStream);
const encodeText = isFetchSupported && (typeof TextEncoder === 'function' ?
((encoder) => (str) => encoder.encode(str))(new TextEncoder()) :
async (str) => new Uint8Array(await new Request(str).arrayBuffer())
);
const supportsRequestStream = isRequestSupported && isReadableStreamSupported && test(() => {
let duplexAccessed = false; let duplexAccessed = false;
const hasContentType = new Request(platform.origin, { const hasContentType = new Request(platform.origin, {
@@ -38,37 +59,39 @@ const supportsRequestStream = isReadableStreamSupported && test(() => {
}).headers.has('Content-Type'); }).headers.has('Content-Type');
return duplexAccessed && !hasContentType; return duplexAccessed && !hasContentType;
}); });
const DEFAULT_CHUNK_SIZE = 64 * 1024; const supportsResponseStream = isResponseSupported && isReadableStreamSupported &&
const supportsResponseStream = isReadableStreamSupported &&
test(() => utils.isReadableStream(new Response('').body)); test(() => utils.isReadableStream(new Response('').body));
const resolvers = {
const resolvers = {
stream: supportsResponseStream && ((res) => res.body) stream: supportsResponseStream && ((res) => res.body)
}; };
isFetchSupported && (((res) => { isFetchSupported && ((() => {
['text', 'arrayBuffer', 'blob', 'formData', 'stream'].forEach(type => { ['text', 'arrayBuffer', 'blob', 'formData', 'stream'].forEach(type => {
!resolvers[type] && (resolvers[type] = utils.isFunction(res[type]) ? (res) => res[type]() : !resolvers[type] && (resolvers[type] = (res, config) => {
(_, config) => { let method = res && res[type];
if (method) {
return method.call(res);
}
throw new AxiosError(`Response type '${type}' is not supported`, AxiosError.ERR_NOT_SUPPORT, config); throw new AxiosError(`Response type '${type}' is not supported`, AxiosError.ERR_NOT_SUPPORT, config);
}) })
}); });
})(new Response)); })());
const getBodyLength = async (body) => { const getBodyLength = async (body) => {
if (body == null) { if (body == null) {
return 0; return 0;
} }
if(utils.isBlob(body)) { if (utils.isBlob(body)) {
return body.size; return body.size;
} }
if(utils.isSpecCompliantForm(body)) { if (utils.isSpecCompliantForm(body)) {
const _request = new Request(platform.origin, { const _request = new Request(platform.origin, {
method: 'POST', method: 'POST',
body, body,
@@ -76,26 +99,26 @@ const getBodyLength = async (body) => {
return (await _request.arrayBuffer()).byteLength; return (await _request.arrayBuffer()).byteLength;
} }
if(utils.isArrayBufferView(body) || utils.isArrayBuffer(body)) { if (utils.isArrayBufferView(body) || utils.isArrayBuffer(body)) {
return body.byteLength; return body.byteLength;
} }
if(utils.isURLSearchParams(body)) { if (utils.isURLSearchParams(body)) {
body = body + ''; body = body + '';
} }
if(utils.isString(body)) { if (utils.isString(body)) {
return (await encodeText(body)).byteLength; return (await encodeText(body)).byteLength;
} }
} }
const resolveBodyLength = async (headers, body) => { const resolveBodyLength = async (headers, body) => {
const length = utils.toFiniteNumber(headers.getContentLength()); const length = utils.toFiniteNumber(headers.getContentLength());
return length == null ? getBodyLength(body) : length; return length == null ? getBodyLength(body) : length;
} }
export default isFetchSupported && (async (config) => { return async (config) => {
let { let {
url, url,
method, method,
@@ -115,7 +138,7 @@ export default isFetchSupported && (async (config) => {
let composedSignal = composeSignals([signal, cancelToken && cancelToken.toAbortSignal()], timeout); let composedSignal = composeSignals([signal, cancelToken && cancelToken.toAbortSignal()], timeout);
let request; let request = null;
const unsubscribe = composedSignal && composedSignal.unsubscribe && (() => { const unsubscribe = composedSignal && composedSignal.unsubscribe && (() => {
composedSignal.unsubscribe(); composedSignal.unsubscribe();
@@ -156,8 +179,9 @@ export default isFetchSupported && (async (config) => {
// Cloudflare Workers throws when credentials are defined // Cloudflare Workers throws when credentials are defined
// see https://github.com/cloudflare/workerd/issues/902 // see https://github.com/cloudflare/workerd/issues/902
const isCredentialsSupported = "credentials" in Request.prototype; const isCredentialsSupported = isRequestSupported && "credentials" in Request.prototype;
request = new Request(url, {
const resolvedOptions = {
...fetchOptions, ...fetchOptions,
signal: composedSignal, signal: composedSignal,
method: method.toUpperCase(), method: method.toUpperCase(),
@@ -165,9 +189,11 @@ export default isFetchSupported && (async (config) => {
body: data, body: data,
duplex: "half", duplex: "half",
credentials: isCredentialsSupported ? withCredentials : undefined credentials: isCredentialsSupported ? withCredentials : undefined
}); };
let response = await fetch(request, fetchOptions); request = isRequestSupported && new Request(url, resolvedOptions);
let response = await (isRequestSupported ? fetch(request, fetchOptions) : fetch(url, resolvedOptions));
const isStreamResponse = supportsResponseStream && (responseType === 'stream' || responseType === 'response'); const isStreamResponse = supportsResponseStream && (responseType === 'stream' || responseType === 'response');
@@ -224,6 +250,37 @@ export default isFetchSupported && (async (config) => {
throw AxiosError.from(err, err && err.code, config, request); throw AxiosError.from(err, err && err.code, config, request);
} }
}); }
}
const seedCache = new Map();
export const getFetch = (config) => {
let env = utils.merge.call({
skipUndefined: true
}, globalFetchAPI, config ? config.env : null);
const {fetch, Request, Response} = env;
const seeds = [
Request, Response, fetch
];
let len = seeds.length, i = len,
seed, target, map = seedCache;
while (i--) {
seed = seeds[i];
target = map.get(seed);
target === undefined && map.set(seed, target = (i ? new Map() : factory(env)))
map = target;
}
return target;
};
const adapter = getFetch();
export default adapter;
+1 -1
View File
@@ -46,7 +46,7 @@ export default function dispatchRequest(config) {
config.headers.setContentType('application/x-www-form-urlencoded', false); config.headers.setContentType('application/x-www-form-urlencoded', false);
} }
const adapter = adapters.getAdapter(config.adapter || defaults.adapter); const adapter = adapters.getAdapter(config.adapter || defaults.adapter, config);
return adapter(config).then(function onAdapterResolution(response) { return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config); throwIfCancellationRequested(config);
+3 -1
View File
@@ -341,7 +341,7 @@ const isContextDefined = (context) => !isUndefined(context) && context !== _glob
* @returns {Object} Result of all merge properties * @returns {Object} Result of all merge properties
*/ */
function merge(/* obj1, obj2, obj3, ... */) { function merge(/* obj1, obj2, obj3, ... */) {
const {caseless} = isContextDefined(this) && this || {}; const {caseless, skipUndefined} = isContextDefined(this) && this || {};
const result = {}; const result = {};
const assignValue = (val, key) => { const assignValue = (val, key) => {
const targetKey = caseless && findKey(result, key) || key; const targetKey = caseless && findKey(result, key) || key;
@@ -352,9 +352,11 @@ function merge(/* obj1, obj2, obj3, ... */) {
} else if (isArray(val)) { } else if (isArray(val)) {
result[targetKey] = val.slice(); result[targetKey] = val.slice();
} else { } else {
if (!skipUndefined || !isUndefined(val)) {
result[targetKey] = val; result[targetKey] = val;
} }
} }
}
for (let i = 0, l = arguments.length; i < l; i++) { for (let i = 0, l = arguments.length; i < l; i++) {
arguments[i] && forEach(arguments[i], assignValue); arguments[i] && forEach(arguments[i], assignValue);
+81
View File
@@ -409,4 +409,85 @@ describe('supports fetch with nodejs', function () {
await fetchAxios.post('/form', form); await fetchAxios.post('/form', form);
}); });
}); });
describe('env config', () => {
it('should respect env fetch API configuration', async() => {
const { data, headers } = await fetchAxios.get('/', {
env: {
fetch() {
return {
headers: {
foo: '1'
},
text: async () => 'test'
}
}
}
});
assert.strictEqual(headers.get('foo'), '1');
assert.strictEqual(data, 'test');
});
it('should be able to request with lack of Request object', async() => {
const form = new FormData();
form.append('x', '1');
const { data, headers } = await fetchAxios.post('/', form, {
onUploadProgress() {
// dummy listener to activate streaming
},
env: {
Request: null,
fetch() {
return {
headers: {
foo: '1'
},
text: async () => 'test'
}
}
}
});
assert.strictEqual(headers.get('foo'), '1');
assert.strictEqual(data, 'test');
});
it('should be able to handle response with lack of Response object', async() => {
const { data, headers } = await fetchAxios.get('/', {
onDownloadProgress() {
// dummy listener to activate streaming
},
env: {
Request: null,
Response: null,
fetch() {
return {
headers: {
foo: '1'
},
text: async () => 'test'
}
}
}
});
assert.strictEqual(headers.get('foo'), '1');
assert.strictEqual(data, 'test');
});
it('should fallback to the global on undefined env value', async() => {
server = await startHTTPServer((req, res) => res.end('OK'));
const { data } = await fetchAxios.get('/', {
env: {
fetch: undefined
}
});
assert.strictEqual(data, 'OK');
});
});
}); });