This commit is contained in:
Bruno Oliveira 2026-07-11 13:33:22 +03:00 committed by GitHub
commit d86a4aad76
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 154 additions and 4 deletions

View File

@ -2,8 +2,11 @@
## Unreleased
- Add `TestServer::{query, squery}()` for issuing HTTP `QUERY` requests (RFC 10008). [#4140]
- Minimum supported Rust version (MSRV) is now 1.88.
[#4140]: https://github.com/actix/actix-web/pull/4140
## 3.2.0
- Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency.

View File

@ -216,6 +216,16 @@ impl TestServer {
self.client.patch(self.surl(path.as_ref()).as_str())
}
/// Create `QUERY` request
pub fn query<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.query(self.url(path.as_ref()).as_str())
}
/// Create HTTPS `QUERY` request
pub fn squery<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.query(self.surl(path.as_ref()).as_str())
}
/// Create `DELETE` request
pub fn delete<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.delete(self.url(path.as_ref()).as_str())

View File

@ -2,8 +2,11 @@
## Unreleased
- Add `TestServer::query()` for issuing HTTP `QUERY` requests (RFC 10008). [#4140]
- Minimum supported Rust version (MSRV) is now 1.88.
[#4140]: https://github.com/actix/actix-web/pull/4140
## 0.1.5
- Add `TestServerConfig::listen_address()` method.

View File

@ -697,6 +697,11 @@ impl TestServer {
self.client.patch(self.url(path.as_ref()).as_str())
}
/// Create `QUERY` request.
pub fn query(&self, path: impl AsRef<str>) -> ClientRequest {
self.client.query(self.url(path.as_ref()).as_str())
}
/// Create `DELETE` request.
pub fn delete(&self, path: impl AsRef<str>) -> ClientRequest {
self.client.delete(self.url(path.as_ref()).as_str())

View File

@ -2,8 +2,11 @@
## Unreleased
- Add `#[query]` macro for routing the HTTP `QUERY` method (RFC 10008). [#4140]
- Minimum supported Rust version (MSRV) is now 1.88.
[#4140]: https://github.com/actix/actix-web/pull/4140
## 4.3.0
- Add `#[scope]` macro.

View File

@ -21,7 +21,8 @@
//! There is a macro to set up a handler for each of the most common HTTP methods that also define
//! additional guards and route-specific middleware.
//!
//! See docs for: [GET], [POST], [PATCH], [PUT], [DELETE], [HEAD], [CONNECT], [OPTIONS], [TRACE]
//! See docs for: [GET], [POST], [PATCH], [PUT], [DELETE], [HEAD], [CONNECT], [OPTIONS], [TRACE],
//! [QUERY]
//!
//! ```
//! # use actix_web::HttpResponse;
@ -71,6 +72,7 @@
//! [TRACE]: macro@trace
//! [PATCH]: macro@patch
//! [DELETE]: macro@delete
//! [QUERY]: macro@query
#![recursion_limit = "512"]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
@ -195,6 +197,7 @@ method_macro!(Connect, connect);
method_macro!(Options, options);
method_macro!(Trace, trace);
method_macro!(Patch, patch);
method_macro!(Query, query);
/// Prepends a path prefix to all handlers using routing macros inside the attached module.
///

View File

@ -101,6 +101,7 @@ standard_method_type! {
Options, OPTIONS, options,
Trace, TRACE, trace,
Patch, PATCH, patch,
Query, QUERY, query,
}
impl TryFrom<&syn::LitStr> for MethodType {

View File

@ -11,7 +11,7 @@ use actix_web::{
web, App, Error, HttpRequest, HttpResponse, Responder,
};
use actix_web_codegen::{
connect, delete, get, head, options, patch, post, put, route, routes, trace,
connect, delete, get, head, options, patch, post, put, query, route, routes, trace,
};
use futures_core::future::LocalBoxFuture;
@ -36,6 +36,11 @@ async fn patch_test() -> impl Responder {
HttpResponse::Ok()
}
#[query("/test")]
async fn query_test() -> impl Responder {
HttpResponse::Ok()
}
#[post("/test")]
async fn post_test() -> impl Responder {
HttpResponse::NoContent()
@ -258,6 +263,7 @@ async fn test_body() {
.service(options_test)
.service(trace_test)
.service(patch_test)
.service(query_test)
.service(test_handler)
.service(route_test)
.service(routes_overlapping_test)
@ -290,6 +296,13 @@ async fn test_body() {
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(
http::Method::from_bytes(b"QUERY").unwrap(),
srv.url("/test"),
);
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::PUT, srv.url("/test"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());

View File

@ -2,6 +2,10 @@
## Unreleased
- Add support for the HTTP `QUERY` method (RFC 10008): `web::query()`, `guard::Query()`, `Resource::query()`, and `TestRequest::query()`. [#4140]
[#4140]: https://github.com/actix/actix-web/pull/4140
## 4.14.0
- Add `HttpRequest::{cookies_raw,cookie_raw}` and `ServiceRequest::{cookies_raw,cookie_raw}` for reading request cookies without percent-decoding names and values. [#3542]

View File

@ -425,6 +425,9 @@ impl Guard for MethodGuard {
macro_rules! method_guard {
($method_fn:ident, $method_const:ident) => {
method_guard!($method_fn, $method_const, HttpMethod::$method_const);
};
($method_fn:ident, $method_const:ident, $method:expr) => {
#[doc = concat!("Creates a guard that matches the `", stringify!($method_const), "` request method.")]
///
/// # Examples
@ -438,7 +441,7 @@ macro_rules! method_guard {
/// ```
#[allow(non_snake_case)]
pub fn $method_fn() -> impl Guard {
MethodGuard(HttpMethod::$method_const)
MethodGuard($method)
}
};
}
@ -452,6 +455,9 @@ method_guard!(Options, OPTIONS);
method_guard!(Connect, CONNECT);
method_guard!(Patch, PATCH);
method_guard!(Trace, TRACE);
// `QUERY` (RFC 10008) is a safe, idempotent method that carries a request body.
// TODO: replace with `HttpMethod::QUERY` once the `http` dependency is bumped (see #3384).
method_guard!(Query, QUERY, HttpMethod::from_bytes(b"QUERY").unwrap());
/// Creates a guard that matches if request contains given header name and value.
///
@ -543,6 +549,10 @@ mod tests {
assert!(Patch().check(&req.guard_ctx()));
assert!(!Patch().check(&get_req.guard_ctx()));
let req = TestRequest::query().to_srv_request();
assert!(Query().check(&req.guard_ctx()));
assert!(!Query().check(&get_req.guard_ctx()));
let r = TestRequest::delete().to_srv_request();
assert!(Delete().check(&r.guard_ctx()));
assert!(!Delete().check(&get_req.guard_ctx()));

View File

@ -155,6 +155,7 @@ codegen_reexport!(delete);
codegen_reexport!(trace);
codegen_reexport!(connect);
codegen_reexport!(options);
codegen_reexport!(query);
codegen_reexport!(scope);
pub(crate) type BoxError = Box<dyn std::error::Error>;

View File

@ -403,6 +403,7 @@ where
route_shortcut!(delete, "DELETE");
route_shortcut!(head, "HEAD");
route_shortcut!(trace, "TRACE");
route_shortcut!(query, "QUERY");
}
impl<T, B> HttpServiceFactory for Resource<T>
@ -810,6 +811,32 @@ mod tests {
assert_eq!(resp.status(), StatusCode::NO_CONTENT);
}
#[actix_rt::test]
async fn test_query_method() {
let srv = init_service(
App::new().service(web::resource("/test").route(web::query().to(HttpResponse::Ok))),
)
.await;
// a QUERY request is routed to the `web::query()` handler
let req = TestRequest::with_uri("/test")
.method(Method::from_bytes(b"QUERY").unwrap())
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
// any other method is rejected with `405 Method Not Allowed`
let req = TestRequest::with_uri("/test")
.method(Method::GET)
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
assert_eq!(
resp.headers().get(header::ALLOW).unwrap().as_bytes(),
b"QUERY"
);
}
// allow deprecated `{App, Resource}::data`
#[allow(deprecated)]
#[actix_rt::test]

View File

@ -111,6 +111,12 @@ impl TestRequest {
TestRequest::default().method(Method::PATCH)
}
/// Constructs test request with QUERY method.
// TODO: use `Method::QUERY` once the `http` dependency is bumped (see #3384).
pub fn query() -> TestRequest {
TestRequest::default().method(Method::from_bytes(b"QUERY").unwrap())
}
/// Constructs test request with DELETE method.
pub fn delete() -> TestRequest {
TestRequest::default().method(Method::DELETE)

View File

@ -371,6 +371,7 @@ mod tests {
web::resource("/index.html")
.route(web::put().to(|| HttpResponse::Ok().body("put!")))
.route(web::patch().to(|| HttpResponse::Ok().body("patch!")))
.route(web::query().to(|| HttpResponse::Ok().body("query!")))
.route(web::delete().to(|| HttpResponse::Ok().body("delete!"))),
),
)
@ -392,11 +393,42 @@ mod tests {
let result = call_and_read_body(&app, patch_req).await;
assert_eq!(result, Bytes::from_static(b"patch!"));
let query_req = TestRequest::query()
.uri("/index.html")
.insert_header((header::CONTENT_TYPE, "application/json"))
.to_request();
let result = call_and_read_body(&app, query_req).await;
assert_eq!(result, Bytes::from_static(b"query!"));
let delete_req = TestRequest::delete().uri("/index.html").to_request();
let result = call_and_read_body(&app, delete_req).await;
assert_eq!(result, Bytes::from_static(b"delete!"));
}
// `QUERY` is safe and idempotent like `GET`, but (like `POST`) carries a request body;
// this proves body extractors work for `QUERY` handlers. See RFC 10008.
#[actix_rt::test]
async fn test_query_carries_json_body() {
let app =
init_service(App::new().service(web::resource("/people").route(
web::query().to(|person: web::Json<Person>| HttpResponse::Ok().json(person)),
)))
.await;
let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes();
let req = TestRequest::query()
.uri("/people")
.insert_header((header::CONTENT_TYPE, "application/json"))
.set_payload(payload)
.to_request();
let result: Person = call_and_read_body_json(&app, req).await;
assert_eq!(result.id, "12345");
assert_eq!(result.name, "User name");
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Person {
id: String,

View File

@ -101,6 +101,9 @@ pub fn route() -> Route {
macro_rules! method_route {
($method_fn:ident, $method_const:ident) => {
method_route!($method_fn, $method_const, Method::$method_const);
};
($method_fn:ident, $method_const:ident, $method:expr) => {
#[doc = concat!(" Creates a new route with `", stringify!($method_const), "` method guard.")]
///
/// # Examples
@ -115,7 +118,7 @@ macro_rules! method_route {
/// );
/// ```
pub fn $method_fn() -> Route {
method(Method::$method_const)
method($method)
}
};
}
@ -127,6 +130,9 @@ method_route!(patch, PATCH);
method_route!(delete, DELETE);
method_route!(head, HEAD);
method_route!(trace, TRACE);
// `QUERY` (RFC 10008) is a safe, idempotent method that carries a request body.
// TODO: replace with `Method::QUERY` once the `http` dependency is bumped (see #3384).
method_route!(query, QUERY, Method::from_bytes(b"QUERY").unwrap());
/// Creates a new route with specified method guard.
///

View File

@ -2,11 +2,13 @@
## Unreleased
- Add `Client::query()` for constructing HTTP `QUERY` requests (RFC 10008). [#4140]
- Add camel-case header controls to `WebsocketsRequest` via `camel_case_headers()` and `set_camel_case_headers()`. [#3953]
- Update `hickory-resolver` dependency to `0.26.1`.
- Update `rand` dependency to `0.10`.
[#3953]: https://github.com/actix/actix-web/pull/3953
[#4140]: https://github.com/actix/actix-web/pull/4140
## 3.8.2

View File

@ -181,6 +181,16 @@ impl Client {
self.request(Method::OPTIONS, url)
}
/// Construct HTTP *QUERY* request.
// TODO: use `Method::QUERY` once the `http` dependency is bumped (see #3384).
pub fn query<U>(&self, url: U) -> ClientRequest
where
Uri: TryFrom<U>,
<Uri as TryFrom<U>>::Error: Into<HttpError>,
{
self.request(Method::from_bytes(b"QUERY").unwrap(), url)
}
/// Initialize a WebSocket connection.
/// Returns a WebSocket connection builder.
pub fn ws<U>(&self, url: U) -> ws::WebsocketsRequest
@ -203,3 +213,14 @@ impl Client {
Rc::get_mut(&mut self.0.default_headers)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn client_query_builds_query_request() {
let req = Client::new().query("/");
assert_eq!(req.get_method().as_str(), "QUERY");
}
}