2
0
mirror of https://github.com/tenrok/axios.git synced 2026-06-17 19:21:29 +03:00

feat: add QUERY HTTP method support (#10802)

* feat: add QUERY HTTP method support

Add support for the HTTP QUERY method as defined in
draft-ietf-httpbis-safe-method-w-body. QUERY is a safe, idempotent
method like GET but carries a request body, making it suitable for
complex queries that cannot be expressed in a URL.

Changes:
- Add axios.query(url, data, config) and axios.queryForm() shorthands
- Register 'query' in default headers initialization
- Include 'query' in header cleanup during request dispatch
- Add 'QUERY' to Method type in TypeScript definitions (d.ts and d.cts)
- Add query/queryForm signatures to Axios class type definitions
- Add 'query' to HeadersDefaults interface
- Update unit and module typing tests

Closes #5465

Signed-off-by: Pierluigi Lenoci <pierluigilenoci@gmail.com>

* test: add thorough QUERY method tests

Add comprehensive tests for the QUERY HTTP method covering:
- Request method correctness (via mock adapter and real HTTP server)
- Request body support (QUERY accepts a body like POST/PUT/PATCH)
- Custom headers handling
- baseURL configuration with instances
- Content-Type auto-detection (application/json for objects)
- Instance method and defaults merging
- Generic request form axios({ method: 'query' })
- queryForm() multipart/form-data support
- Integration tests against a real HTTP server verifying the QUERY
  method string arrives correctly on the wire

Signed-off-by: Pierluigi Lenoci <pierluigilenoci@gmail.com>

* chore: updated docs with all translations

* chore: drop formquery method as this is probably not a real use case

* chore: remove un-needed file

---------

Signed-off-by: Pierluigi Lenoci <pierluigilenoci@gmail.com>
Co-authored-by: Jay <jasonsaayman@gmail.com>
This commit is contained in:
Pierluigi Lenoci
2026-04-28 14:28:30 +02:00
committed by GitHub
parent bd88727198
commit f39203dcbe
13 changed files with 405 additions and 6 deletions
@@ -80,6 +80,26 @@ El método `patch` se usa para hacer una solicitud PATCH. Acepta una URL, un obj
axios.patch(url: string, data?: D, config?: AxiosRequestConfig<C>): AxiosResponse<R>;
```
### `query`
El método `query` se usa para hacer una solicitud QUERY, un método seguro e idempotente que transporta un cuerpo. Acepta una URL, un objeto de datos opcional y un objeto de configuración opcional como argumentos, y devuelve una Promise que se resuelve en el objeto de respuesta. Úsalo para operaciones de tipo lectura cuyos parámetros sean demasiado complejos o sensibles para ir en la URL.
```ts
axios.query(url: string, data?: D, config?: AxiosRequestConfig<C>): AxiosResponse<R>;
```
```js
// Enviar un filtro de búsqueda complejo como cuerpo de la solicitud
const { data } = await axios.query("/api/search", {
selector: ["name", "email"],
filter: { active: true, role: "admin" },
});
```
::: warning Especificación en borrador
El método QUERY está definido por un [Internet-Draft](https://datatracker.ietf.org/doc/draft-ietf-httpbis-safe-method-w-body/) del IETF y todavía no ha sido estandarizado. La semántica e incluso el propio nombre del método pueden cambiar antes de la publicación final, y el soporte en servidores, proxies y CDN es desigual. Verifica que tu infraestructura acepte `QUERY` de extremo a extremo antes de usarlo en producción.
:::
## Métodos abreviados para datos de formulario
Estos métodos son equivalentes a sus contrapartes anteriores, pero predefinen `Content-Type` como `multipart/form-data`. Son la forma recomendada de subir archivos o enviar formularios HTML.
@@ -80,6 +80,26 @@ La méthode `patch` est utilisée pour effectuer une requête PATCH. Elle accept
axios.patch(url: string, data?: D, config?: AxiosRequestConfig<C>): AxiosResponse<R>;
```
### `query`
La méthode `query` est utilisée pour effectuer une requête QUERY, une méthode sûre et idempotente qui transporte un corps. Elle accepte une URL, un objet de données optionnel et un objet de configuration optionnel en arguments et retourne une promise qui se résout vers l'objet de réponse. Utilisez-la pour des opérations de type lecture dont les paramètres sont trop complexes ou trop sensibles pour figurer dans l'URL.
```ts
axios.query(url: string, data?: D, config?: AxiosRequestConfig<C>): AxiosResponse<R>;
```
```js
// Envoyer un filtre de recherche complexe dans le corps de la requête
const { data } = await axios.query("/api/search", {
selector: ["name", "email"],
filter: { active: true, role: "admin" },
});
```
::: warning Spécification en cours d'élaboration
La méthode QUERY est définie par un [Internet-Draft](https://datatracker.ietf.org/doc/draft-ietf-httpbis-safe-method-w-body/) de l'IETF et n'a pas encore été standardisée. La sémantique et le nom même de la méthode peuvent évoluer avant la publication finale, et la prise en charge par les serveurs, proxys et CDN est inégale. Vérifiez que votre pile accepte `QUERY` de bout en bout avant de vous en servir en production.
:::
## Méthodes raccourcies pour les données de formulaire
Ces méthodes sont équivalentes à leurs homologues ci-dessus, mais prédéfinissent le `Content-Type` à `multipart/form-data`. Elles constituent la façon recommandée d'envoyer des fichiers ou de soumettre des formulaires HTML.
@@ -80,6 +80,26 @@ The `patch` method is used to make a PATCH request. It takes a URL, an optional
axios.patch(url: string, data?: D, config?: AxiosRequestConfig<C>): AxiosResponse<R>;
```
### `query`
The `query` method is used to make a QUERY request, a safe and idempotent method that carries a body. It takes a URL, an optional data object, and an optional configuration object as arguments and returns a promise that resolves to the response object. Use it for read-style operations whose parameters are too complex or sensitive to fit in the URL.
```ts
axios.query(url: string, data?: D, config?: AxiosRequestConfig<C>): AxiosResponse<R>;
```
```js
// Send a complex search filter as a request body
const { data } = await axios.query("/api/search", {
selector: ["name", "email"],
filter: { active: true, role: "admin" },
});
```
::: warning Draft specification
The QUERY method is defined by an IETF [Internet-Draft](https://datatracker.ietf.org/doc/draft-ietf-httpbis-safe-method-w-body/) and has not yet been standardized. Semantics and the method name itself may change before final publication, and server, proxy, and CDN support is uneven. Verify your stack accepts `QUERY` end to end before relying on it in production.
:::
## Form data shorthand methods
These methods are equivalent to their counterparts above, but preset `Content-Type` to `multipart/form-data`. They are the recommended way to upload files or submit HTML forms.
@@ -80,6 +80,26 @@ axios.put(url: string, data?: D, config?: AxiosRequestConfig<C>): AxiosResponse<
axios.patch(url: string, data?: D, config?: AxiosRequestConfig<C>): AxiosResponse<R>;
```
### `query`
`query` 方法用于发起 QUERY 请求,这是一种安全且幂等的、可以携带请求体的方法。它接受 URL、可选数据对象和可选配置对象,返回解析为响应对象的 Promise。当读取类操作的参数过于复杂或敏感、不适合放在 URL 中时,可以使用该方法。
```ts
axios.query(url: string, data?: D, config?: AxiosRequestConfig<C>): AxiosResponse<R>;
```
```js
// 将复杂的搜索条件作为请求体发送
const { data } = await axios.query("/api/search", {
selector: ["name", "email"],
filter: { active: true, role: "admin" },
});
```
::: warning 草案规范
QUERY 方法目前由 IETF 的 [Internet-Draft](https://datatracker.ietf.org/doc/draft-ietf-httpbis-safe-method-w-body/) 定义,尚未成为正式标准。其语义乃至方法名称都可能在最终发布前发生变化,并且服务器、代理和 CDN 的支持情况参差不齐。在用于生产环境之前,请确认你的整个链路都能够正确处理 `QUERY` 请求。
:::
## 表单数据快捷方法
这些方法与上述对应方法等价,但会预设 `Content-Type``multipart/form-data`,是上传文件或提交 HTML 表单的推荐方式。
+8 -1
View File
@@ -221,6 +221,11 @@ declare class Axios {
data?: D,
config?: axios.AxiosRequestConfig<D>
): Promise<R>;
query<T = any, R = axios.AxiosResponse<T>, D = any>(
url: string,
data?: D,
config?: axios.AxiosRequestConfig<D>
): Promise<R>;
}
declare enum HttpStatusCode {
@@ -359,7 +364,8 @@ declare namespace axios {
| 'PATCH'
| 'PURGE'
| 'LINK'
| 'UNLINK';
| 'UNLINK'
| 'QUERY';
type Method = (UppercaseMethod | Lowercase<UppercaseMethod>) & {};
@@ -566,6 +572,7 @@ declare namespace axios {
purge?: RawAxiosRequestHeaders;
link?: RawAxiosRequestHeaders;
unlink?: RawAxiosRequestHeaders;
query?: RawAxiosRequestHeaders;
}
interface AxiosDefaults<D = any> extends Omit<AxiosRequestConfig<D>, 'headers'> {
Vendored
+8 -1
View File
@@ -243,7 +243,8 @@ type UppercaseMethod =
| 'PATCH'
| 'PURGE'
| 'LINK'
| 'UNLINK';
| 'UNLINK'
| 'QUERY';
export type Method = (UppercaseMethod | Lowercase<UppercaseMethod>) & {};
@@ -467,6 +468,7 @@ export interface HeadersDefaults {
purge?: RawAxiosRequestHeaders;
link?: RawAxiosRequestHeaders;
unlink?: RawAxiosRequestHeaders;
query?: RawAxiosRequestHeaders;
}
export interface AxiosDefaults<D = any> extends Omit<AxiosRequestConfig<D>, 'headers'> {
@@ -650,6 +652,11 @@ export class Axios {
data?: D,
config?: AxiosRequestConfig<D>
): Promise<R>;
query<T = any, R = AxiosResponse<T>, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>
): Promise<R>;
}
export interface AxiosInstance extends Axios {
+7 -3
View File
@@ -148,7 +148,7 @@ class Axios {
let contextHeaders = headers && utils.merge(headers.common, headers[config.method]);
headers &&
utils.forEach(['delete', 'get', 'head', 'post', 'put', 'patch', 'common'], (method) => {
utils.forEach(['delete', 'get', 'head', 'post', 'put', 'patch', 'query', 'common'], (method) => {
delete headers[method];
});
@@ -251,7 +251,7 @@ utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData
};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
utils.forEach(['post', 'put', 'patch', 'query'], function forEachMethodWithData(method) {
function generateHTTPMethod(isForm) {
return function httpMethod(url, data, config) {
return this.request(
@@ -271,7 +271,11 @@ utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
Axios.prototype[method] = generateHTTPMethod();
Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
// QUERY is a safe/idempotent read method; multipart form bodies don't fit
// its semantics, so no queryForm shorthand is generated.
if (method !== 'query') {
Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
}
});
export default Axios;
+1 -1
View File
@@ -170,7 +170,7 @@ const defaults = {
},
};
utils.forEach(['delete', 'get', 'head', 'post', 'put', 'patch'], (method) => {
utils.forEach(['delete', 'get', 'head', 'post', 'put', 'patch', 'query'], (method) => {
defaults.headers[method] = {};
});
@@ -91,6 +91,8 @@ axios.put('/user', { foo: 'bar' }).then(handleResponse).catch(handleError);
axios.patch('/user', { foo: 'bar' }).then(handleResponse).catch(handleError);
axios.query('/user', { foo: 'bar' }).then(handleResponse).catch(handleError);
// Typed methods
interface UserCreationDef {
name: string;
@@ -113,6 +113,8 @@ axios.put('/user', { foo: 'bar' }).then(handleResponse).catch(handleError);
axios.patch('/user', { foo: 'bar' }).then(handleResponse).catch(handleError);
axios.query('/user', { foo: 'bar' }).then(handleResponse).catch(handleError);
// Typed methods
interface UserCreationDef {
name: string;
+27
View File
@@ -621,6 +621,33 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () =>
}
});
it('should send QUERY requests with a body through the fetch adapter', async () => {
const server = await startHTTPServer(
(req, res) => {
let body = '';
req.on('data', (chunk) => { body += chunk; });
req.on('end', () => {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ method: req.method, url: req.url, body }));
});
},
{ port: 0 }
);
try {
const { data } = await fetchAxios.query(
`http://localhost:${server.address().port}/search`,
{ selector: 'field1' }
);
assert.strictEqual(data.method, 'QUERY');
assert.strictEqual(data.url, '/search');
assert.deepStrictEqual(JSON.parse(data.body), { selector: 'field1' });
} finally {
await stopHTTPServer(server);
}
});
it('should support params', async () => {
const server = await startHTTPServer((req, res) => res.end(req.url), { port: SERVER_PORT });
try {
+2
View File
@@ -12,6 +12,7 @@ describe('static api', () => {
assert.strictEqual(typeof axios.post, 'function');
assert.strictEqual(typeof axios.put, 'function');
assert.strictEqual(typeof axios.patch, 'function');
assert.strictEqual(typeof axios.query, 'function');
});
it('should have promise method helpers', async () => {
@@ -93,6 +94,7 @@ describe('instance api', () => {
assert.strictEqual(typeof instance.post, 'function');
assert.strictEqual(typeof instance.put, 'function');
assert.strictEqual(typeof instance.patch, 'function');
assert.strictEqual(typeof instance.query, 'function');
});
it('should have interceptors', () => {
+268
View File
@@ -0,0 +1,268 @@
import { describe, it } from 'vitest';
import assert from 'assert';
import axios from '../../index.js';
import { startHTTPServer, stopHTTPServer } from '../setup/server.js';
describe('QUERY method', () => {
describe('static axios.query()', () => {
it('should make a request with the QUERY HTTP method', async () => {
const response = await axios.query('/test', null, {
adapter: (config) => {
assert.strictEqual(config.method, 'query');
return Promise.resolve({
data: null,
status: 200,
statusText: 'OK',
headers: {},
config,
request: {},
});
},
});
assert.strictEqual(response.status, 200);
});
it('should support a request body', async () => {
const requestBody = { selector: 'field1, field2', filter: { active: true } };
await axios.query('/search', requestBody, {
adapter: (config) => {
assert.deepStrictEqual(config.data, JSON.stringify(requestBody));
return Promise.resolve({
data: null,
status: 200,
statusText: 'OK',
headers: {},
config,
request: {},
});
},
});
});
it('should support custom headers', async () => {
await axios.query('/test', null, {
headers: {
'X-Custom-Header': 'custom-value',
Authorization: 'Bearer token-abc',
},
adapter: (config) => {
assert.strictEqual(config.headers.get('X-Custom-Header'), 'custom-value');
assert.strictEqual(config.headers.get('Authorization'), 'Bearer token-abc');
return Promise.resolve({
data: null,
status: 200,
statusText: 'OK',
headers: {},
config,
request: {},
});
},
});
});
it('should work with baseURL configuration', async () => {
const instance = axios.create({ baseURL: 'http://example.com/api' });
await instance.query('/resources', { fields: ['name'] }, {
adapter: (config) => {
assert.strictEqual(config.baseURL, 'http://example.com/api');
assert.strictEqual(config.url, '/resources');
assert.strictEqual(config.method, 'query');
return Promise.resolve({
data: null,
status: 200,
statusText: 'OK',
headers: {},
config,
request: {},
});
},
});
});
it('should set Content-Type to application/json for object bodies', async () => {
await axios.query('/test', { key: 'value' }, {
adapter: (config) => {
assert.ok(
config.headers.get('Content-Type').includes('application/json'),
'Expected Content-Type to include application/json'
);
return Promise.resolve({
data: null,
status: 200,
statusText: 'OK',
headers: {},
config,
request: {},
});
},
});
});
});
describe('instance.query()', () => {
it('should make a request with the QUERY HTTP method on an instance', async () => {
const instance = axios.create();
const response = await instance.query('/test', null, {
adapter: (config) => {
assert.strictEqual(config.method, 'query');
return Promise.resolve({
data: null,
status: 200,
statusText: 'OK',
headers: {},
config,
request: {},
});
},
});
assert.strictEqual(response.status, 200);
});
it('should merge instance defaults with request config', async () => {
const instance = axios.create({
headers: { 'X-Instance-Header': 'from-instance' },
});
await instance.query('/test', null, {
headers: { 'X-Request-Header': 'from-request' },
adapter: (config) => {
assert.strictEqual(config.headers.get('X-Instance-Header'), 'from-instance');
assert.strictEqual(config.headers.get('X-Request-Header'), 'from-request');
return Promise.resolve({
data: null,
status: 200,
statusText: 'OK',
headers: {},
config,
request: {},
});
},
});
});
});
describe('axios({ method: "query" })', () => {
it('should support the generic request form', async () => {
const response = await axios({
method: 'query',
url: '/test',
data: { selector: '*' },
adapter: (config) => {
assert.strictEqual(config.method, 'query');
assert.deepStrictEqual(config.data, JSON.stringify({ selector: '*' }));
return Promise.resolve({
data: { result: 'ok' },
status: 200,
statusText: 'OK',
headers: {},
config,
request: {},
});
},
});
assert.deepStrictEqual(response.data, { result: 'ok' });
});
});
describe('with HTTP server', () => {
it('should send QUERY requests with a body to a real server', async () => {
const server = await startHTTPServer(
(req, res) => {
let body = '';
req.on('data', (chunk) => { body += chunk; });
req.on('end', () => {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
method: req.method,
url: req.url,
body,
headers: req.headers,
}));
});
},
{ port: 0 }
);
try {
const { data } = await axios.query(
`http://localhost:${server.address().port}/search`,
{ selector: 'field1' }
);
assert.strictEqual(data.method, 'QUERY');
assert.strictEqual(data.url, '/search');
const parsedBody = JSON.parse(data.body);
assert.deepStrictEqual(parsedBody, { selector: 'field1' });
assert.ok(
data.headers['content-type'].includes('application/json'),
'Expected server to receive application/json content-type'
);
} finally {
await stopHTTPServer(server);
}
});
it('should send QUERY requests with custom headers to a real server', async () => {
const server = await startHTTPServer(
(req, res) => {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
method: req.method,
headers: req.headers,
}));
},
{ port: 0 }
);
try {
const { data } = await axios.query(
`http://localhost:${server.address().port}/test`,
null,
{
headers: {
'X-Custom': 'test-value',
},
}
);
assert.strictEqual(data.method, 'QUERY');
assert.strictEqual(data.headers['x-custom'], 'test-value');
} finally {
await stopHTTPServer(server);
}
});
it('should send QUERY requests with baseURL to a real server', async () => {
const server = await startHTTPServer(
(req, res) => {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
method: req.method,
url: req.url,
}));
},
{ port: 0 }
);
try {
const instance = axios.create({
baseURL: `http://localhost:${server.address().port}/api`,
});
const { data } = await instance.query('/resources', { fields: ['name'] });
assert.strictEqual(data.method, 'QUERY');
assert.strictEqual(data.url, '/api/resources');
} finally {
await stopHTTPServer(server);
}
});
});
});