Replace Axios with the Fetch API (#3899)

This commit is contained in:
Jon Koops 2022-11-30 17:46:17 +01:00 committed by GitHub
parent 4b3eb4a9e4
commit 7eb77cc0a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 159 additions and 135 deletions

View file

@ -1,6 +1,5 @@
import { NetworkError } from "@keycloak/keycloak-admin-client";
import { AlertVariant } from "@patternfly/react-core"; import { AlertVariant } from "@patternfly/react-core";
import type { AxiosError } from "axios";
import axios from "axios";
import { FunctionComponent, useCallback, useMemo, useState } from "react"; import { FunctionComponent, useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -85,8 +84,8 @@ function getErrorMessage(error: unknown) {
return error; return error;
} }
if (axios.isAxiosError(error)) { if (error instanceof NetworkError) {
return getErrorMessageAxios(error); return getNetworkErrorMessage(error);
} }
if (error instanceof Error) { if (error instanceof Error) {
@ -96,8 +95,8 @@ function getErrorMessage(error: unknown) {
throw new Error("Unable to determine error message."); throw new Error("Unable to determine error message.");
} }
function getErrorMessageAxios(error: AxiosError) { function getNetworkErrorMessage({ responseData }: NetworkError) {
const data = (error.response?.data ?? {}) as Record<string, unknown>; const data = responseData as Record<string, unknown>;
for (const key of ["error_description", "errorMessage", "error"]) { for (const key of ["error_description", "errorMessage", "error"]) {
const value = data[key]; const value = data[key];

View file

@ -1,3 +1,4 @@
import { NetworkError } from "@keycloak/keycloak-admin-client";
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation"; import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
import { sortBy } from "lodash-es"; import { sortBy } from "lodash-es";
import { import {
@ -7,7 +8,6 @@ import {
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import axios from "axios";
import { RecentUsed } from "../components/realm-selector/recent-used"; import { RecentUsed } from "../components/realm-selector/recent-used";
import { createNamedContext } from "../utils/createNamedContext"; import { createNamedContext } from "../utils/createNamedContext";
@ -46,11 +46,7 @@ export const RealmsProvider: FunctionComponent = ({ children }) => {
try { try {
return await adminClient.realms.find({ briefRepresentation: true }); return await adminClient.realms.find({ briefRepresentation: true });
} catch (error) { } catch (error) {
if ( if (error instanceof NetworkError && error.response.status < 500) {
axios.isAxiosError(error) &&
error.response &&
error.response.status < 500
) {
return []; return [];
} }

View file

@ -1,5 +1,4 @@
import KeycloakAdminClient from "@keycloak/keycloak-admin-client"; import KeycloakAdminClient from "@keycloak/keycloak-admin-client";
import axios from "axios";
import Keycloak from "keycloak-js"; import Keycloak from "keycloak-js";
import { DependencyList, useEffect } from "react"; import { DependencyList, useEffect } from "react";
import { useErrorHandler } from "react-error-boundary"; import { useErrorHandler } from "react-error-boundary";
@ -39,35 +38,24 @@ export function useFetch<T>(
callback: (param: T) => void, callback: (param: T) => void,
deps?: DependencyList deps?: DependencyList
) { ) {
const { adminClient } = useAdminClient();
const onError = useErrorHandler(); const onError = useErrorHandler();
const controller = new AbortController();
const { signal } = controller;
useEffect(() => { useEffect(() => {
const source = axios.CancelToken.source();
adminClient.setConfig({
requestConfig: { cancelToken: source.token },
});
adminClientCall() adminClientCall()
.then((result) => { .then((result) => {
if (!source.token.reason) { if (!signal.aborted) {
callback(result); callback(result);
} }
}) })
.catch((error) => { .catch((error) => {
if (!axios.isCancel(error)) { if (!signal.aborted) {
onError(error); onError(error);
} }
}); });
adminClient.setConfig({ return () => controller.abort();
requestConfig: { cancelToken: undefined },
});
return () => {
source.cancel();
};
}, deps); }, deps);
} }

View file

@ -22,8 +22,8 @@ import KcAdminClient from '@keycloak/keycloak-admin-client';
// { // {
// baseUrl: 'http://127.0.0.1:8080', // baseUrl: 'http://127.0.0.1:8080',
// realmName: 'master', // realmName: 'master',
// requestConfig: { // requestOptions: {
// /* Axios request config options https://github.com/axios/axios#request-config */ // /* Fetch request options https://developer.mozilla.org/en-US/docs/Web/API/fetch#options */
// }, // },
// } // }
const kcAdminClient = new KcAdminClient(); const kcAdminClient = new KcAdminClient();

View file

@ -37,7 +37,6 @@
} }
}, },
"dependencies": { "dependencies": {
"axios": "^0.27.2",
"camelize-ts": "^2.1.1", "camelize-ts": "^2.1.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"url-join": "^5.0.0", "url-join": "^5.0.0",

View file

@ -1,4 +1,3 @@
import type { AxiosRequestConfig } from "axios";
import type { RequestArgs } from "./resources/agent.js"; import type { RequestArgs } from "./resources/agent.js";
import { AttackDetection } from "./resources/attackDetection.js"; import { AttackDetection } from "./resources/attackDetection.js";
import { AuthenticationManagement } from "./resources/authenticationManagement.js"; import { AuthenticationManagement } from "./resources/authenticationManagement.js";
@ -26,7 +25,7 @@ export interface TokenProvider {
export interface ConnectionConfig { export interface ConnectionConfig {
baseUrl?: string; baseUrl?: string;
realmName?: string; realmName?: string;
requestConfig?: AxiosRequestConfig; requestOptions?: RequestInit;
requestArgOptions?: Pick<RequestArgs, "catchNotFound">; requestArgOptions?: Pick<RequestArgs, "catchNotFound">;
} }
@ -55,14 +54,14 @@ export class KeycloakAdminClient {
public accessToken?: string; public accessToken?: string;
public refreshToken?: string; public refreshToken?: string;
private requestConfig?: AxiosRequestConfig; private requestOptions?: RequestInit;
private globalRequestArgOptions?: Pick<RequestArgs, "catchNotFound">; private globalRequestArgOptions?: Pick<RequestArgs, "catchNotFound">;
private tokenProvider?: TokenProvider; private tokenProvider?: TokenProvider;
constructor(connectionConfig?: ConnectionConfig) { constructor(connectionConfig?: ConnectionConfig) {
this.baseUrl = connectionConfig?.baseUrl || defaultBaseUrl; this.baseUrl = connectionConfig?.baseUrl || defaultBaseUrl;
this.realmName = connectionConfig?.realmName || defaultRealm; this.realmName = connectionConfig?.realmName || defaultRealm;
this.requestConfig = connectionConfig?.requestConfig; this.requestOptions = connectionConfig?.requestOptions;
this.globalRequestArgOptions = connectionConfig?.requestArgOptions; this.globalRequestArgOptions = connectionConfig?.requestArgOptions;
// Initialize resources // Initialize resources
@ -89,7 +88,7 @@ export class KeycloakAdminClient {
baseUrl: this.baseUrl, baseUrl: this.baseUrl,
realmName: this.realmName, realmName: this.realmName,
credentials, credentials,
requestConfig: this.requestConfig, requestOptions: this.requestOptions,
}); });
this.accessToken = accessToken; this.accessToken = accessToken;
this.refreshToken = refreshToken; this.refreshToken = refreshToken;
@ -115,8 +114,8 @@ export class KeycloakAdminClient {
return this.accessToken; return this.accessToken;
} }
public getRequestConfig() { public getRequestOptions() {
return this.requestConfig; return this.requestOptions;
} }
public getGlobalRequestArgOptions(): public getGlobalRequestArgOptions():
@ -139,6 +138,6 @@ export class KeycloakAdminClient {
) { ) {
this.realmName = connectionConfig.realmName; this.realmName = connectionConfig.realmName;
} }
this.requestConfig = connectionConfig.requestConfig; this.requestOptions = connectionConfig.requestOptions;
} }
} }

View file

@ -1,5 +1,7 @@
import { RequiredActionAlias } from "./defs/requiredActionProviderRepresentation.js";
import { KeycloakAdminClient } from "./client.js"; import { KeycloakAdminClient } from "./client.js";
import { RequiredActionAlias } from "./defs/requiredActionProviderRepresentation.js";
export const requiredAction = RequiredActionAlias; export const requiredAction = RequiredActionAlias;
export default KeycloakAdminClient; export default KeycloakAdminClient;
export { NetworkError } from "./utils/fetchWithError.js";
export type { NetworkErrorOptions } from "./utils/fetchWithError.js";

View file

@ -1,13 +1,19 @@
import axios, { AxiosRequestConfig, AxiosRequestHeaders, Method } from "axios";
import { isUndefined, last, omit, pick } from "lodash-es"; import { isUndefined, last, omit, pick } from "lodash-es";
import urlJoin from "url-join"; import urlJoin from "url-join";
import { parseTemplate } from "url-template"; import { parseTemplate } from "url-template";
import type { KeycloakAdminClient } from "../client.js"; import type { KeycloakAdminClient } from "../client.js";
import {
fetchWithError,
NetworkError,
parseResponse,
} from "../utils/fetchWithError.js";
import { stringifyQueryParams } from "../utils/stringifyQueryParams.js"; import { stringifyQueryParams } from "../utils/stringifyQueryParams.js";
// constants // constants
const SLASH = "/"; const SLASH = "/";
type Method = "GET" | "POST" | "PUT" | "DELETE";
// interface // interface
export interface RequestArgs { export interface RequestArgs {
method: Method; method: Method;
@ -31,7 +37,7 @@ export interface RequestArgs {
* Keys to be ignored, meaning that they will not be filtered out of the request payload even if they are a part of `urlParamKeys` or `queryParamKeys`, * Keys to be ignored, meaning that they will not be filtered out of the request payload even if they are a part of `urlParamKeys` or `queryParamKeys`,
*/ */
ignoredKeys?: string[]; ignoredKeys?: string[];
headers?: AxiosRequestHeaders; headers?: HeadersInit;
} }
export class Agent { export class Agent {
@ -76,7 +82,9 @@ export class Agent {
const baseParams = this.getBaseParams?.() ?? {}; const baseParams = this.getBaseParams?.() ?? {};
// Filter query parameters by queryParamKeys // Filter query parameters by queryParamKeys
const queryParams = queryParamKeys ? pick(payload, queryParamKeys) : null; const queryParams = queryParamKeys
? pick(payload, queryParamKeys)
: undefined;
// Add filtered payload parameters to base parameters // Add filtered payload parameters to base parameters
const allUrlParamKeys = [...Object.keys(baseParams), ...urlParamKeys]; const allUrlParamKeys = [...Object.keys(baseParams), ...urlParamKeys];
@ -128,7 +136,9 @@ export class Agent {
const baseParams = this.getBaseParams?.() ?? {}; const baseParams = this.getBaseParams?.() ?? {};
// Filter query parameters by queryParamKeys // Filter query parameters by queryParamKeys
const queryParams = queryParamKeys ? pick(query, queryParamKeys) : null; const queryParams = queryParamKeys
? pick(query, queryParamKeys)
: undefined;
// Add filtered query parameters to base parameters // Add filtered query parameters to base parameters
const allUrlParamKeys = [...Object.keys(baseParams), ...urlParamKeys]; const allUrlParamKeys = [...Object.keys(baseParams), ...urlParamKeys];
@ -171,64 +181,72 @@ export class Agent {
path: string; path: string;
payload: any; payload: any;
urlParams: any; urlParams: any;
queryParams?: Record<string, any> | null; queryParams?: Record<string, string>;
catchNotFound: boolean; catchNotFound: boolean;
payloadKey?: string; payloadKey?: string;
returnResourceIdInLocationHeader?: { field: string }; returnResourceIdInLocationHeader?: { field: string };
headers?: AxiosRequestHeaders; headers?: HeadersInit;
}) { }) {
const newPath = urlJoin(this.basePath, path); const newPath = urlJoin(this.basePath, path);
// Parse template and replace with values from urlParams // Parse template and replace with values from urlParams
const pathTemplate = parseTemplate(newPath); const pathTemplate = parseTemplate(newPath);
const parsedPath = pathTemplate.expand(urlParams); const parsedPath = pathTemplate.expand(urlParams);
const url = `${this.getBaseUrl?.() ?? ""}${parsedPath}`; const url = new URL(`${this.getBaseUrl?.() ?? ""}${parsedPath}`);
const requestOptions = { ...this.client.getRequestOptions() };
const requestHeaders = new Headers([
...new Headers(requestOptions.headers).entries(),
["authorization", `Bearer ${await this.client.getAccessToken()}`],
["accept", "application/json, text/plain, */*"],
...new Headers(headers).entries(),
]);
// Prepare request config const searchParams: Record<string, string> = {};
const requestConfig: AxiosRequestConfig = {
paramsSerializer: (params) => stringifyQueryParams(params),
...(this.client.getRequestConfig() || {}),
method,
url,
};
// Headers // Add payload parameters to search params if method is 'GET'.
requestConfig.headers = {
...requestConfig.headers,
Authorization: `bearer ${await this.client.getAccessToken()}`,
...headers,
};
// Put payload into querystring if method is GET
if (method === "GET") { if (method === "GET") {
requestConfig.params = payload; Object.assign(searchParams, payload);
} else if (requestHeaders.get("content-type") === "text/plain") {
// Pass the payload as a plain string if the content type is 'text/plain'.
requestOptions.body = payload as unknown as string;
} else { } else {
// Set the request data to the payload, or the value corresponding to the payloadKey, if it's defined // Otherwise assume it's JSON and stringify it.
requestConfig.data = payloadKey ? payload[payloadKey] : payload; requestOptions.body = JSON.stringify(
payloadKey ? payload[payloadKey] : payload
);
} }
// Concat to existing queryParams if (!requestHeaders.has("content-type")) {
if (queryParams) { requestHeaders.set("content-type", "application/json");
requestConfig.params = requestConfig.params
? {
...requestConfig.params,
...queryParams,
} }
: queryParams;
if (queryParams) {
Object.assign(searchParams, queryParams);
}
url.search = stringifyQueryParams(searchParams);
if (!requestHeaders.has("content-type")) {
requestHeaders.set("content-type", "application/x-www-form-urlencoded");
} }
try { try {
const res = await axios.default(requestConfig); const res = await fetchWithError(url, {
...requestOptions,
headers: requestHeaders,
method,
});
// now we get the response of the http request // now we get the response of the http request
// if `resourceIdInLocationHeader` is true, we'll get the resourceId from the location header field // if `resourceIdInLocationHeader` is true, we'll get the resourceId from the location header field
// todo: find a better way to find the id in path, maybe some kind of pattern matching // todo: find a better way to find the id in path, maybe some kind of pattern matching
// for now, we simply split the last sub-path of the path returned in location header field // for now, we simply split the last sub-path of the path returned in location header field
if (returnResourceIdInLocationHeader) { if (returnResourceIdInLocationHeader) {
const locationHeader = res.headers.location; const locationHeader = res.headers.get("location");
if (typeof locationHeader !== "string") { if (typeof locationHeader !== "string") {
throw new Error( throw new Error(
`location header is not found in request: ${res.config.url}` `location header is not found in request: ${res.url}`
); );
} }
@ -236,7 +254,7 @@ export class Agent {
if (!resourceId) { if (!resourceId) {
// throw an error to let users know the response is not expected // throw an error to let users know the response is not expected
throw new Error( throw new Error(
`resourceId is not found in Location header from request: ${res.config.url}` `resourceId is not found in Location header from request: ${res.url}`
); );
} }
@ -244,11 +262,12 @@ export class Agent {
const { field } = returnResourceIdInLocationHeader; const { field } = returnResourceIdInLocationHeader;
return { [field]: resourceId }; return { [field]: resourceId };
} }
return res.data;
return parseResponse(res);
} catch (err) { } catch (err) {
if ( if (
axios.default.isAxiosError(err) && err instanceof NetworkError &&
err.response?.status === 404 && err.response.status === 404 &&
catchNotFound catchNotFound
) { ) {
return null; return null;

View file

@ -223,7 +223,6 @@ export class Users extends Resource<{ realm?: string }> {
urlParamKeys: ["id"], urlParamKeys: ["id"],
payloadKey: "actions", payloadKey: "actions",
queryParamKeys: ["lifespan", "redirectUri", "clientId"], queryParamKeys: ["lifespan", "redirectUri", "clientId"],
headers: { "content-type": "application/json" },
keyTransform: { keyTransform: {
clientId: "client_id", clientId: "client_id",
redirectUri: "redirect_uri", redirectUri: "redirect_uri",

View file

@ -1,6 +1,6 @@
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
import camelize from "camelize-ts"; import camelize from "camelize-ts";
import { defaultBaseUrl, defaultRealm } from "./constants.js"; import { defaultBaseUrl, defaultRealm } from "./constants.js";
import { fetchWithError } from "./fetchWithError.js";
import { stringifyQueryParams } from "./stringifyQueryParams.js"; import { stringifyQueryParams } from "./stringifyQueryParams.js";
export type GrantTypes = "client_credentials" | "password" | "refresh_token"; export type GrantTypes = "client_credentials" | "password" | "refresh_token";
@ -20,7 +20,7 @@ export interface Settings {
realmName?: string; realmName?: string;
baseUrl?: string; baseUrl?: string;
credentials: Credentials; credentials: Credentials;
requestConfig?: AxiosRequestConfig; requestOptions?: RequestInit;
} }
export interface TokenResponseRaw { export interface TokenResponseRaw {
@ -69,20 +69,25 @@ export const getToken = async (settings: Settings): Promise<TokenResponse> => {
: {}), : {}),
}); });
const config: AxiosRequestConfig = { const options = settings.requestOptions ?? {};
...settings.requestConfig, const headers = new Headers(options.headers);
};
if (credentials.clientSecret) { if (credentials.clientSecret) {
config.auth = { headers.set(
username: credentials.clientId, "Authorization",
password: credentials.clientSecret, atob(credentials.clientId + ":" + credentials.clientSecret)
}; );
} }
const { data } = await axios.default.post< headers.set("content-type", "application/x-www-form-urlencoded");
any,
AxiosResponse<TokenResponseRaw> const response = await fetchWithError(url, {
>(url, payload, config); ...options,
method: "POST",
headers,
body: payload,
});
const data: TokenResponseRaw = await response.json();
return camelize(data); return camelize(data);
}; };

View file

@ -0,0 +1,44 @@
export type NetworkErrorOptions = { response: Response; responseData: unknown };
export class NetworkError extends Error {
response: Response;
responseData: unknown;
constructor(message: string, options: NetworkErrorOptions) {
super(message);
this.response = options.response;
this.responseData = options.responseData;
}
}
export async function fetchWithError(
input: RequestInfo | URL,
init?: RequestInit
) {
const response = await fetch(input, init);
if (!response.ok) {
const responseData = await parseResponse(response);
throw new NetworkError("Network response was not OK.", {
response,
responseData,
});
}
return response;
}
export async function parseResponse(response: Response): Promise<any> {
if (!response.body) {
return "";
}
const data = await response.text();
try {
return JSON.parse(data);
// eslint-disable-next-line no-empty
} catch (error) {}
return data;
}

56
package-lock.json generated
View file

@ -548,7 +548,6 @@
"version": "999.0.0-dev", "version": "999.0.0-dev",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"axios": "^0.27.2",
"camelize-ts": "^2.1.1", "camelize-ts": "^2.1.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"url-join": "^5.0.0", "url-join": "^5.0.0",
@ -5691,6 +5690,7 @@
}, },
"node_modules/asynckit": { "node_modules/asynckit": {
"version": "0.4.0", "version": "0.4.0",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/at-least-node": { "node_modules/at-least-node": {
@ -5732,14 +5732,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/axios": {
"version": "0.27.2",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
}
},
"node_modules/babel-loader": { "node_modules/babel-loader": {
"version": "8.2.5", "version": "8.2.5",
"dev": true, "dev": true,
@ -6685,6 +6677,7 @@
}, },
"node_modules/combined-stream": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
@ -7597,6 +7590,7 @@
}, },
"node_modules/delayed-stream": { "node_modules/delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.4.0" "node": ">=0.4.0"
@ -9065,24 +9059,6 @@
"tabbable": "^5.3.2" "tabbable": "^5.3.2"
} }
}, },
"node_modules/follow-redirects": {
"version": "1.15.1",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-in": { "node_modules/for-in": {
"version": "1.0.2", "version": "1.0.2",
"dev": true, "dev": true,
@ -9101,6 +9077,7 @@
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.0", "version": "4.0.0",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
@ -11233,6 +11210,7 @@
}, },
"node_modules/mime-db": { "node_modules/mime-db": {
"version": "1.52.0", "version": "1.52.0",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@ -11240,6 +11218,7 @@
}, },
"node_modules/mime-types": { "node_modules/mime-types": {
"version": "2.1.35", "version": "2.1.35",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"mime-db": "1.52.0" "mime-db": "1.52.0"
@ -17819,7 +17798,6 @@
"@types/lodash-es": "^4.17.5", "@types/lodash-es": "^4.17.5",
"@types/mocha": "^10.0.1", "@types/mocha": "^10.0.1",
"@types/node": "^18.0.3", "@types/node": "^18.0.3",
"axios": "^0.27.2",
"camelize-ts": "^2.1.1", "camelize-ts": "^2.1.1",
"chai": "^4.3.7", "chai": "^4.3.7",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
@ -19854,7 +19832,8 @@
"optional": true "optional": true
}, },
"asynckit": { "asynckit": {
"version": "0.4.0" "version": "0.4.0",
"dev": true
}, },
"at-least-node": { "at-least-node": {
"version": "1.0.0", "version": "1.0.0",
@ -19875,13 +19854,6 @@
"version": "1.11.0", "version": "1.11.0",
"dev": true "dev": true
}, },
"axios": {
"version": "0.27.2",
"requires": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
}
},
"babel-loader": { "babel-loader": {
"version": "8.2.5", "version": "8.2.5",
"dev": true, "dev": true,
@ -20516,6 +20488,7 @@
}, },
"combined-stream": { "combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"dev": true,
"requires": { "requires": {
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
} }
@ -21123,7 +21096,8 @@
} }
}, },
"delayed-stream": { "delayed-stream": {
"version": "1.0.0" "version": "1.0.0",
"dev": true
}, },
"deprecation": { "deprecation": {
"version": "2.3.1" "version": "2.3.1"
@ -22140,9 +22114,6 @@
"tabbable": "^5.3.2" "tabbable": "^5.3.2"
} }
}, },
"follow-redirects": {
"version": "1.15.1"
},
"for-in": { "for-in": {
"version": "1.0.2", "version": "1.0.2",
"dev": true "dev": true
@ -22153,6 +22124,7 @@
}, },
"form-data": { "form-data": {
"version": "4.0.0", "version": "4.0.0",
"dev": true,
"requires": { "requires": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
@ -23570,10 +23542,12 @@
} }
}, },
"mime-db": { "mime-db": {
"version": "1.52.0" "version": "1.52.0",
"dev": true
}, },
"mime-types": { "mime-types": {
"version": "2.1.35", "version": "2.1.35",
"dev": true,
"requires": { "requires": {
"mime-db": "1.52.0" "mime-db": "1.52.0"
} }