diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index be19d251a..ba5a97893 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -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. diff --git a/actix-http-test/src/lib.rs b/actix-http-test/src/lib.rs index e3ea69e5c..f99554291 100644 --- a/actix-http-test/src/lib.rs +++ b/actix-http-test/src/lib.rs @@ -216,6 +216,16 @@ impl TestServer { self.client.patch(self.surl(path.as_ref()).as_str()) } + /// Create `QUERY` request + pub fn query>(&self, path: S) -> ClientRequest { + self.client.query(self.url(path.as_ref()).as_str()) + } + + /// Create HTTPS `QUERY` request + pub fn squery>(&self, path: S) -> ClientRequest { + self.client.query(self.surl(path.as_ref()).as_str()) + } + /// Create `DELETE` request pub fn delete>(&self, path: S) -> ClientRequest { self.client.delete(self.url(path.as_ref()).as_str()) diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index fc64cb7a3..45251d84a 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -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. diff --git a/actix-test/src/lib.rs b/actix-test/src/lib.rs index 84adacbce..10ec32936 100644 --- a/actix-test/src/lib.rs +++ b/actix-test/src/lib.rs @@ -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) -> ClientRequest { + self.client.query(self.url(path.as_ref()).as_str()) + } + /// Create `DELETE` request. pub fn delete(&self, path: impl AsRef) -> ClientRequest { self.client.delete(self.url(path.as_ref()).as_str()) diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index 6138892a1..062a76aa4 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -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. diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index f6ca56aa0..31372f1ee 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -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. /// diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index c2f7dbb31..7cef917af 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -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 { diff --git a/actix-web-codegen/tests/routes.rs b/actix-web-codegen/tests/routes.rs index 1443f9a75..0b1cdd5de 100644 --- a/actix-web-codegen/tests/routes.rs +++ b/actix-web-codegen/tests/routes.rs @@ -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()); diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index 97ad926a1..4fe7da2ac 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -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] diff --git a/actix-web/src/guard/mod.rs b/actix-web/src/guard/mod.rs index 60bb116bf..c402e989a 100644 --- a/actix-web/src/guard/mod.rs +++ b/actix-web/src/guard/mod.rs @@ -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())); diff --git a/actix-web/src/lib.rs b/actix-web/src/lib.rs index 53ee93c83..fd54e3ea2 100644 --- a/actix-web/src/lib.rs +++ b/actix-web/src/lib.rs @@ -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; diff --git a/actix-web/src/resource.rs b/actix-web/src/resource.rs index 441b05bae..0a2a2cb48 100644 --- a/actix-web/src/resource.rs +++ b/actix-web/src/resource.rs @@ -403,6 +403,7 @@ where route_shortcut!(delete, "DELETE"); route_shortcut!(head, "HEAD"); route_shortcut!(trace, "TRACE"); + route_shortcut!(query, "QUERY"); } impl HttpServiceFactory for Resource @@ -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] diff --git a/actix-web/src/test/test_request.rs b/actix-web/src/test/test_request.rs index f178d6f43..e39a8e88e 100644 --- a/actix-web/src/test/test_request.rs +++ b/actix-web/src/test/test_request.rs @@ -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) diff --git a/actix-web/src/test/test_utils.rs b/actix-web/src/test/test_utils.rs index 4540d8a6b..4ee9e9a4f 100644 --- a/actix-web/src/test/test_utils.rs +++ b/actix-web/src/test/test_utils.rs @@ -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| 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, diff --git a/actix-web/src/web.rs b/actix-web/src/web.rs index 92d05eb74..c956d7f15 100644 --- a/actix-web/src/web.rs +++ b/actix-web/src/web.rs @@ -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. /// diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 9888c66c3..9ed8f9094 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -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 diff --git a/awc/src/client/mod.rs b/awc/src/client/mod.rs index c9fa37253..d17729d50 100644 --- a/awc/src/client/mod.rs +++ b/awc/src/client/mod.rs @@ -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(&self, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + self.request(Method::from_bytes(b"QUERY").unwrap(), url) + } + /// Initialize a WebSocket connection. /// Returns a WebSocket connection builder. pub fn ws(&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"); + } +}