mirror of
https://github.com/tenrok/axios.git
synced 2026-06-17 19:21:29 +03:00
fix: support URL object as config.url input (#10866)
* fix(buildURL): support URL object as input Fixes #6546 When passing a URL object (e.g., new URL(...)) to axios methods like axios.get(url, { params: {...} }), the buildURL function would crash with 'url.indexOf is not a function' because it assumed url was always a string. This fix converts URL objects to strings before processing. * fix: support URL object as config.url input - Move URL object coercion from buildURL to Axios._request so that all downstream consumers (buildFullPath, combineURLs, adapters) see a string. This fixes the crash when using URL object with baseURL (combineURLs calls .replace() on the URL object). - Remove the broken buildURL unit test that tested buildURL(url, null) without actually exercising URL coercion (buildURL early-returns when !params). - Add e2e tests via the HTTP adapter for URL object with params and URL object without params (no crash). * feat: note + example added under axios(url[, config]) documenting URL support * feat: url widened to string | URL * feat: oercion now uses config.url instanceof url * chore: add additional test cases to improve coverage * fix: apply suggestions from cubic review --------- Co-authored-by: liuwei53 <liuwei53@baidu.com> Co-authored-by: Jason Saayman <jasonsaayman@gmail.com>
This commit is contained in:
@@ -592,11 +592,20 @@ response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'));
|
||||
|
||||
##### axios(url[, config])
|
||||
|
||||
`url` accepts either a string or a [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) instance. The `URL` is coerced to a string before the request is dispatched.
|
||||
|
||||
```js
|
||||
// Send a GET request (default method)
|
||||
axios('/user/12345');
|
||||
```
|
||||
|
||||
```js
|
||||
// `url` may also be a URL object
|
||||
axios.get(new URL('https://api.example.com/foo'), {
|
||||
params: { a: 1 },
|
||||
});
|
||||
```
|
||||
|
||||
### Request method aliases
|
||||
|
||||
For convenience, aliases have been provided for all common request methods.
|
||||
|
||||
+13
-13
@@ -176,53 +176,53 @@ declare class Axios {
|
||||
config: axios.AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
get<T = any, R = axios.AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
config?: axios.AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
delete<T = any, R = axios.AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
config?: axios.AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
head<T = any, R = axios.AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
config?: axios.AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
options<T = any, R = axios.AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
config?: axios.AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
post<T = any, R = axios.AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
data?: D,
|
||||
config?: axios.AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
put<T = any, R = axios.AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
data?: D,
|
||||
config?: axios.AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
patch<T = any, R = axios.AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
data?: D,
|
||||
config?: axios.AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
postForm<T = any, R = axios.AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
data?: D,
|
||||
config?: axios.AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
putForm<T = any, R = axios.AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
data?: D,
|
||||
config?: axios.AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
patchForm<T = any, R = axios.AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
data?: D,
|
||||
config?: axios.AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
query<T = any, R = axios.AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
data?: D,
|
||||
config?: axios.AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
@@ -473,7 +473,7 @@ declare namespace axios {
|
||||
type LookupAddress = string | LookupAddressEntry;
|
||||
|
||||
interface AxiosRequestConfig<D = any> {
|
||||
url?: string;
|
||||
url?: string | URL;
|
||||
method?: Method | string;
|
||||
baseURL?: string;
|
||||
allowAbsoluteUrls?: boolean;
|
||||
@@ -661,7 +661,7 @@ declare namespace axios {
|
||||
interface AxiosInstance extends Axios {
|
||||
<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
|
||||
<T = any, R = AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
config?: AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
|
||||
|
||||
Vendored
+13
-13
@@ -364,7 +364,7 @@ export interface LookupAddressEntry {
|
||||
export type LookupAddress = string | LookupAddressEntry;
|
||||
|
||||
export interface AxiosRequestConfig<D = any> {
|
||||
url?: string;
|
||||
url?: string | URL;
|
||||
method?: StringLiteralsOrString<Method>;
|
||||
baseURL?: string;
|
||||
allowAbsoluteUrls?: boolean;
|
||||
@@ -609,53 +609,53 @@ export class Axios {
|
||||
getUri(config?: AxiosRequestConfig): string;
|
||||
request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
|
||||
get<T = any, R = AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
config?: AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
delete<T = any, R = AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
config?: AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
head<T = any, R = AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
config?: AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
options<T = any, R = AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
config?: AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
post<T = any, R = AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
data?: D,
|
||||
config?: AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
put<T = any, R = AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
data?: D,
|
||||
config?: AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
patch<T = any, R = AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
data?: D,
|
||||
config?: AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
postForm<T = any, R = AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
data?: D,
|
||||
config?: AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
putForm<T = any, R = AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
data?: D,
|
||||
config?: AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
patchForm<T = any, R = AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
data?: D,
|
||||
config?: AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
query<T = any, R = AxiosResponse<T>, D = any>(
|
||||
url: string,
|
||||
url: string | URL,
|
||||
data?: D,
|
||||
config?: AxiosRequestConfig<D>
|
||||
): Promise<R>;
|
||||
@@ -663,7 +663,7 @@ export class Axios {
|
||||
|
||||
export interface AxiosInstance extends Axios {
|
||||
<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
|
||||
<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
|
||||
<T = any, R = AxiosResponse<T>, D = any>(url: string | URL, config?: AxiosRequestConfig<D>): Promise<R>;
|
||||
|
||||
create(config?: CreateAxiosDefaults): AxiosInstance;
|
||||
defaults: Omit<AxiosDefaults, 'headers'> & {
|
||||
|
||||
+25
-3
@@ -12,6 +12,19 @@ import transitionalDefaults from '../defaults/transitional.js';
|
||||
|
||||
const validators = validator.validators;
|
||||
|
||||
const { toString } = Object.prototype;
|
||||
|
||||
const isURL = (url) => {
|
||||
if (url === null || typeof url !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const prototype = Object.getPrototypeOf(url);
|
||||
return prototype !== null && prototype !== Object.prototype && toString.call(url) === '[object URL]';
|
||||
};
|
||||
|
||||
const normalizeURL = (url) => (isURL(url) ? url.toString() : url);
|
||||
|
||||
/**
|
||||
* Create a new instance of Axios
|
||||
*
|
||||
@@ -31,7 +44,7 @@ class Axios {
|
||||
/**
|
||||
* Dispatch a request
|
||||
*
|
||||
* @param {String|Object} configOrUrl The config specific for this request (merged with this.defaults)
|
||||
* @param {String|URL|Object} configOrUrl The config specific for this request (merged with this.defaults)
|
||||
* @param {?Object} config
|
||||
*
|
||||
* @returns {Promise} The Promise to be fulfilled
|
||||
@@ -82,13 +95,19 @@ class Axios {
|
||||
_request(configOrUrl, config) {
|
||||
/*eslint no-param-reassign:0*/
|
||||
// Allow for axios('example/url'[, config]) a la fetch API
|
||||
if (typeof configOrUrl === 'string') {
|
||||
if (typeof configOrUrl === 'string' || isURL(configOrUrl)) {
|
||||
config = config || {};
|
||||
config.url = configOrUrl;
|
||||
config.url = normalizeURL(configOrUrl);
|
||||
} else {
|
||||
config = configOrUrl || {};
|
||||
}
|
||||
|
||||
// Coerce config.url to a string if it's a URL object, so that all downstream
|
||||
// consumers (buildFullPath, combineURLs, adapters) see a string.
|
||||
if (isURL(config.url)) {
|
||||
config.url = normalizeURL(config.url);
|
||||
}
|
||||
|
||||
config = mergeConfig(this.defaults, config);
|
||||
|
||||
const { transitional, paramsSerializer, headers } = config;
|
||||
@@ -232,6 +251,9 @@ class Axios {
|
||||
|
||||
getUri(config) {
|
||||
config = mergeConfig(this.defaults, config);
|
||||
if (isURL(config.url)) {
|
||||
config.url = normalizeURL(config.url);
|
||||
}
|
||||
const fullPath = buildFullPath(config.baseURL, config.url, config.allowAbsoluteUrls);
|
||||
return buildURL(fullPath, config.params, config.paramsSerializer);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,25 @@ import CanceledError from '../cancel/CanceledError.js';
|
||||
import AxiosHeaders from '../core/AxiosHeaders.js';
|
||||
import adapters from '../adapters/adapters.js';
|
||||
|
||||
const { toString } = Object.prototype;
|
||||
|
||||
const isURL = (url) => {
|
||||
if (url === null || typeof url !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const prototype = Object.getPrototypeOf(url);
|
||||
return prototype !== null && prototype !== Object.prototype && toString.call(url) === '[object URL]';
|
||||
};
|
||||
|
||||
const normalizeURL = (url) => (isURL(url) ? url.toString() : url);
|
||||
|
||||
function normalizeConfigURL(config) {
|
||||
if (isURL(config.url)) {
|
||||
config.url = normalizeURL(config.url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws a `CanceledError` if cancellation has been requested.
|
||||
*
|
||||
@@ -32,6 +51,8 @@ function throwIfCancellationRequested(config) {
|
||||
* @returns {Promise} The Promise to be fulfilled
|
||||
*/
|
||||
export default function dispatchRequest(config) {
|
||||
normalizeConfigURL(config);
|
||||
|
||||
throwIfCancellationRequested(config);
|
||||
|
||||
config.headers = AxiosHeaders.from(config.headers);
|
||||
@@ -43,6 +64,8 @@ export default function dispatchRequest(config) {
|
||||
config.headers.setContentType('application/x-www-form-urlencoded', false);
|
||||
}
|
||||
|
||||
normalizeConfigURL(config);
|
||||
|
||||
const adapter = adapters.getAdapter(config.adapter || defaults.adapter, config);
|
||||
|
||||
return adapter(config).then(
|
||||
|
||||
@@ -29,6 +29,12 @@ export function encode(val) {
|
||||
* @returns {string} The formatted url
|
||||
*/
|
||||
export default function buildURL(url, params, options) {
|
||||
// Safeguard for direct callers (e.g. via ./unsafe/helpers/buildURL.js): coerce URL-like
|
||||
// objects to strings before using string methods below.
|
||||
if (url !== null && typeof url === 'object' && typeof url.toString === 'function') {
|
||||
url = url.toString();
|
||||
}
|
||||
|
||||
if (!params) {
|
||||
return url;
|
||||
}
|
||||
|
||||
@@ -3116,6 +3116,46 @@ describe('supports http with nodejs', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should support URL object with params', async () => {
|
||||
const server = await startHTTPServer(
|
||||
(req, res) => {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ url: req.url }));
|
||||
},
|
||||
{ port: SERVER_PORT }
|
||||
);
|
||||
|
||||
try {
|
||||
const url = new URL(`http://localhost:${server.address().port}/foo?a=1`);
|
||||
const response = await axios.get(url, { params: { b: '2' } });
|
||||
|
||||
assert.strictEqual(response.status, 200);
|
||||
assert.strictEqual(response.data.url, '/foo?a=1&b=2');
|
||||
} finally {
|
||||
await stopHTTPServer(server);
|
||||
}
|
||||
});
|
||||
|
||||
it('should support URL object without params (no crash)', async () => {
|
||||
const server = await startHTTPServer(
|
||||
(req, res) => {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ url: req.url }));
|
||||
},
|
||||
{ port: SERVER_PORT }
|
||||
);
|
||||
|
||||
try {
|
||||
const url = new URL(`http://localhost:${server.address().port}/foo`);
|
||||
const response = await axios.get(url);
|
||||
|
||||
assert.strictEqual(response.status, 200);
|
||||
assert.strictEqual(response.data.url, '/foo');
|
||||
} finally {
|
||||
await stopHTTPServer(server);
|
||||
}
|
||||
});
|
||||
|
||||
it('should support HTTP protocol', async () => {
|
||||
const server = await startHTTPServer(
|
||||
(req, res) => {
|
||||
|
||||
@@ -35,6 +35,94 @@ describe('static api', () => {
|
||||
await promise;
|
||||
});
|
||||
|
||||
it('should support URL object shorthand with config', async () => {
|
||||
const url = new URL('http://example.com/test?a=1');
|
||||
|
||||
const response = await axios(url, {
|
||||
params: {
|
||||
b: 2,
|
||||
},
|
||||
adapter: (config) =>
|
||||
Promise.resolve({
|
||||
data: null,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {},
|
||||
config,
|
||||
request: {},
|
||||
}),
|
||||
});
|
||||
|
||||
assert.strictEqual(response.config.url, url.toString());
|
||||
assert.deepStrictEqual(response.config.params, { b: 2 });
|
||||
});
|
||||
|
||||
it('should not require global URL to be a constructor for string requests', async () => {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'URL');
|
||||
|
||||
Object.defineProperty(globalThis, 'URL', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: {},
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await axios('/test', {
|
||||
adapter: (config) =>
|
||||
Promise.resolve({
|
||||
data: null,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {},
|
||||
config,
|
||||
request: {},
|
||||
}),
|
||||
});
|
||||
|
||||
assert.strictEqual(response.config.url, '/test');
|
||||
} finally {
|
||||
if (descriptor) {
|
||||
Object.defineProperty(globalThis, 'URL', descriptor);
|
||||
} else {
|
||||
delete globalThis.URL;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should normalize URL object set by a request interceptor before dispatch', async () => {
|
||||
const url = new URL('http://example.com/interceptor');
|
||||
let transformUrl;
|
||||
const interceptorId = axios.interceptors.request.use((config) => {
|
||||
config.url = url;
|
||||
return config;
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await axios('/test', {
|
||||
transformRequest: [
|
||||
function (data) {
|
||||
transformUrl = this.url;
|
||||
return data;
|
||||
},
|
||||
],
|
||||
adapter: (config) =>
|
||||
Promise.resolve({
|
||||
data: null,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {},
|
||||
config,
|
||||
request: {},
|
||||
}),
|
||||
});
|
||||
|
||||
assert.strictEqual(transformUrl, url.toString());
|
||||
assert.strictEqual(response.config.url, url.toString());
|
||||
} finally {
|
||||
axios.interceptors.request.eject(interceptorId);
|
||||
}
|
||||
});
|
||||
|
||||
it('should have defaults', () => {
|
||||
assert.strictEqual(typeof axios.defaults, 'object');
|
||||
assert.strictEqual(typeof axios.defaults.headers, 'object');
|
||||
@@ -69,6 +157,16 @@ describe('static api', () => {
|
||||
assert.strictEqual(typeof axios.getUri, 'function');
|
||||
});
|
||||
|
||||
it('should support URL object config in getUri', () => {
|
||||
const url = new URL('https://api.example.com/foo');
|
||||
|
||||
assert.strictEqual(axios.getUri({ url }), url.toString());
|
||||
assert.strictEqual(
|
||||
axios.getUri({ baseURL: 'https://example.com/base', url }),
|
||||
url.toString()
|
||||
);
|
||||
});
|
||||
|
||||
it('should have isAxiosError properties', () => {
|
||||
assert.strictEqual(typeof axios.isAxiosError, 'function');
|
||||
});
|
||||
|
||||
@@ -101,6 +101,51 @@ describe('helpers::buildURL', () => {
|
||||
expect(buildURL('/foo', new URLSearchParams('bar=baz'))).toEqual('/foo?bar=baz');
|
||||
});
|
||||
|
||||
it('should support URL object without params', () => {
|
||||
const url = new URL('http://example.com/foo?a=1');
|
||||
expect(buildURL(url)).toEqual('http://example.com/foo?a=1');
|
||||
});
|
||||
|
||||
it('should support URL object with params', () => {
|
||||
const url = new URL('http://example.com/foo?a=1');
|
||||
expect(buildURL(url, { b: 2 })).toEqual('http://example.com/foo?a=1&b=2');
|
||||
});
|
||||
|
||||
it('should support URL-like object with params', () => {
|
||||
const url = {
|
||||
toString() {
|
||||
return 'http://example.com/foo?a=1';
|
||||
},
|
||||
};
|
||||
|
||||
expect(buildURL(url, { b: 2 })).toEqual('http://example.com/foo?a=1&b=2');
|
||||
});
|
||||
|
||||
it('should not require global URL to be a constructor', () => {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'URL');
|
||||
const url = {
|
||||
toString() {
|
||||
return 'http://example.com/foo?a=1';
|
||||
},
|
||||
};
|
||||
|
||||
Object.defineProperty(globalThis, 'URL', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: {},
|
||||
});
|
||||
|
||||
try {
|
||||
expect(buildURL(url, { b: 2 })).toEqual('http://example.com/foo?a=1&b=2');
|
||||
} finally {
|
||||
if (descriptor) {
|
||||
Object.defineProperty(globalThis, 'URL', descriptor);
|
||||
} else {
|
||||
delete globalThis.URL;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should support custom serialize function', () => {
|
||||
const params = {
|
||||
x: 1,
|
||||
|
||||
Reference in New Issue
Block a user