diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index 62ad6f9e7..6440ad693 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -6,8 +6,10 @@ - Add `ErrorHandlers::default_handler()` (as well as `default_handler_{server, client}()`) to make registering handlers for groups of response statuses easier. [#2784] - Add `Logger::custom_response_replace()`. [#2631] - Add rudimentary redirection service at `web::redirect()` / `web::Redirect`. [#1961] +- Add `guard::Acceptable` for matching against `Accept` header mime types. [#2265] [#1961]: https://github.com/actix/actix-web/pull/1961 +[#2265]: https://github.com/actix/actix-web/pull/2265 [#2631]: https://github.com/actix/actix-web/pull/2631 [#2784]: https://github.com/actix/actix-web/pull/2784 [#2867]: https://github.com/actix/actix-web/pull/2867 diff --git a/actix-web/src/guard/acceptable.rs b/actix-web/src/guard/acceptable.rs new file mode 100644 index 000000000..a31494a18 --- /dev/null +++ b/actix-web/src/guard/acceptable.rs @@ -0,0 +1,99 @@ +use super::{Guard, GuardContext}; +use crate::http::header::Accept; + +/// A guard that verifies that an `Accept` header is present and it contains a compatible MIME type. +/// +/// An exception is that matching `*/*` must be explicitly enabled because most browsers send this +/// as part of their `Accept` header for almost every request. +/// +/// # Examples +/// ``` +/// use actix_web::{guard::Acceptable, web, HttpResponse}; +/// +/// web::resource("/images") +/// .guard(Acceptable::new(mime::IMAGE_STAR)) +/// .default_service(web::to(|| async { +/// HttpResponse::Ok().body("only called when images responses are acceptable") +/// })); +/// ``` +#[derive(Debug, Clone)] +pub struct Acceptable { + mime: mime::Mime, + + /// Wether to match `*/*` mime type. + /// + /// Defaults to false because it's not very useful otherwise. + match_star_star: bool, +} + +impl Acceptable { + /// Constructs new `Acceptable` guard with the given `mime` type/pattern. + pub fn new(mime: mime::Mime) -> Self { + Self { + mime, + match_star_star: false, + } + } + + /// Allows `*/*` in the `Accept` header to pass the guard check. + pub fn match_star_star(mut self) -> Self { + self.match_star_star = true; + self + } +} + +impl Guard for Acceptable { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + let accept = match ctx.header::() { + Some(hdr) => hdr, + None => return false, + }; + + let target_type = self.mime.type_(); + let target_subtype = self.mime.subtype(); + + for mime in accept.0.into_iter().map(|q| q.item) { + return match (mime.type_(), mime.subtype()) { + (typ, subtype) if typ == target_type && subtype == target_subtype => true, + (typ, mime::STAR) if typ == target_type => true, + (mime::STAR, mime::STAR) if self.match_star_star => true, + _ => continue, + }; + } + + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{http::header, test::TestRequest}; + + #[test] + fn test_acceptable() { + let req = TestRequest::default().to_srv_request(); + assert!(!Acceptable::new(mime::APPLICATION_JSON).check(&req.guard_ctx())); + + let req = TestRequest::default() + .insert_header((header::ACCEPT, "application/json")) + .to_srv_request(); + assert!(Acceptable::new(mime::APPLICATION_JSON).check(&req.guard_ctx())); + + let req = TestRequest::default() + .insert_header((header::ACCEPT, "text/html, application/json")) + .to_srv_request(); + assert!(Acceptable::new(mime::APPLICATION_JSON).check(&req.guard_ctx())); + } + + #[test] + fn test_acceptable_star() { + let req = TestRequest::default() + .insert_header((header::ACCEPT, "text/html, */*;q=0.8")) + .to_srv_request(); + + assert!(Acceptable::new(mime::APPLICATION_JSON) + .match_star_star() + .check(&req.guard_ctx())); + } +} diff --git a/actix-web/src/guard.rs b/actix-web/src/guard/mod.rs similarity index 99% rename from actix-web/src/guard.rs rename to actix-web/src/guard/mod.rs index ef1301075..5fcaec0de 100644 --- a/actix-web/src/guard.rs +++ b/actix-web/src/guard/mod.rs @@ -56,6 +56,9 @@ use actix_http::{header, uri::Uri, Extensions, Method as HttpMethod, RequestHead use crate::{http::header::Header, service::ServiceRequest, HttpMessage as _}; +mod acceptable; +pub use self::acceptable::Acceptable; + /// Provides access to request parts that are useful during routing. #[derive(Debug)] pub struct GuardContext<'a> { diff --git a/actix-web/src/lib.rs b/actix-web/src/lib.rs index 331d79664..338541208 100644 --- a/actix-web/src/lib.rs +++ b/actix-web/src/lib.rs @@ -107,7 +107,6 @@ pub use crate::error::Result; pub use crate::error::{Error, ResponseError}; pub use crate::extract::FromRequest; pub use crate::handler::Handler; -pub use crate::redirect::Redirect; pub use crate::request::HttpRequest; pub use crate::resource::Resource; pub use crate::response::{CustomizeResponder, HttpResponse, HttpResponseBuilder, Responder}; diff --git a/actix-web/src/redirect.rs b/actix-web/src/redirect.rs index 9e548e707..ca9e23aa4 100644 --- a/actix-web/src/redirect.rs +++ b/actix-web/src/redirect.rs @@ -3,7 +3,6 @@ use std::borrow::Cow; use actix_utils::future::ready; -use log::debug; use crate::{ dev::{fn_service, AppService, HttpServiceFactory, ResourceDef, ServiceRequest}, @@ -13,25 +12,35 @@ use crate::{ /// An HTTP service for redirecting one path to another path or URL. /// -/// Redirects are either [relative](Redirect::to) or [absolute](Redirect::to). -/// /// By default, the "307 Temporary Redirect" status is used when responding. See [this MDN -/// article](mdn-redirects) on why 307 is preferred over 302. +/// article][mdn-redirects] on why 307 is preferred over 302. /// /// # Examples +/// As service: /// ``` /// use actix_web::{web, App}; /// /// App::new() /// // redirect "/duck" to DuckDuckGo -/// .service(web::Redirect::new("/duck", "https://duckduckgo.com/")) +/// .service(web::redirect("/duck", "https://duck.com")) /// .service( -/// // redirect "/api/old" to "/api/new" using `web::redirect` helper +/// // redirect "/api/old" to "/api/new" /// web::scope("/api").service(web::redirect("/old", "/new")) /// ); /// ``` /// -/// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#permanent_redirections +/// As responder: +/// ``` +/// use actix_web::web::Redirect; +/// +/// async fn handler() -> impl Responder { +/// // sends a permanent (308) redirect to duck.com +/// Redirect::to("https://duck.com").permanent() +/// } +/// # actix_web::web::to(handler); +/// ``` +/// +/// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#temporary_redirections #[derive(Debug, Clone)] pub struct Redirect { from: Cow<'static, str>, @@ -40,23 +49,26 @@ pub struct Redirect { } impl Redirect { - /// Create a new `Redirect` service, first providing the path that should be redirected. + /// Construct a new `Redirect` service that matches a path. /// - /// The default "to" location is the root path (`/`). It is expected that you should call either - /// [`to`](Redirect::to) or [`to`](Redirect::to) afterwards. + /// This service will match exact paths equal to `from` within the current scope. I.e., when + /// registered on the root `App`, it will match exact, whole paths. But when registered on a + /// `Scope`, it will match paths under that scope, ignoring the defined scope prefix, just like + /// a normal `Resource` or `Route`. /// - /// Note this function has no effect when used as a responder. + /// The `to` argument can be path or URL; whatever is provided shall be used verbatim when + /// setting the redirect location. This means that relative paths can be used to navigate + /// relatively to matched paths. /// - /// Redirect to an address or path. - /// - /// Whatever argument is provided shall be used as-is when setting the redirect location. - /// You can also use relative paths to navigate relative to the matched path. + /// Prefer [`Redirect::to()`](Self::to) when using `Redirect` as a responder since `from` has + /// no meaning in that context. /// /// # Examples /// ``` - /// # use actix_web::web::Redirect; - /// // redirects "/oh/hi/mark" to "/oh/bye/mark" - /// Redirect::new("/oh/hi/mark", "../../bye/mark"); + /// # use actix_web::{web::Redirect, App}; + /// App::new() + /// // redirects "/oh/hi/mark" to "/oh/bye/johnny" + /// .service(Redirect::new("/oh/hi/mark", "../../bye/johnny")); /// ``` pub fn new(from: impl Into>, to: impl Into>) -> Self { Self { @@ -66,9 +78,20 @@ impl Redirect { } } - /// Shortcut for creating a redirect to use as a `Responder`. + /// Construct a new `Redirect` to use as a responder. /// - /// Only receives a `to` argument since responders do not need to do route matching. + /// Only receives the `to` argument since responders do not need to do route matching. + /// + /// # Examples + /// ``` + /// use actix_web::web::Redirect; + /// + /// async fn admin_page() -> impl Responder { + /// // sends a temporary 307 redirect to the login path + /// Redirect::to("/login") + /// } + /// # actix_web::web::to(handler); + /// ``` pub fn to(to: impl Into>) -> Self { Self { from: "/".into(), @@ -79,7 +102,7 @@ impl Redirect { /// Use the "308 Permanent Redirect" status when responding. /// - /// See [this MDN article](mdn-redirects) on why 308 is preferred over 301. + /// See [this MDN article][mdn-redirects] on why 308 is preferred over 301. /// /// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#permanent_redirections pub fn permanent(self) -> Self { @@ -88,13 +111,20 @@ impl Redirect { /// Use the "307 Temporary Redirect" status when responding. /// - /// See [this MDN article](mdn-redirects) on why 307 is preferred over 302. + /// See [this MDN article][mdn-redirects] on why 307 is preferred over 302. /// /// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#temporary_redirections pub fn temporary(self) -> Self { self.using_status_code(StatusCode::TEMPORARY_REDIRECT) } + /// Use the "303 See Other" status when responding. + /// + /// This status code is semantically correct as the response to a successful login, for example. + pub fn see_other(self) -> Self { + self.using_status_code(StatusCode::SEE_OTHER) + } + /// Allows the use of custom status codes for less common redirect types. /// /// In most cases, the default status ("308 Permanent Redirect") or using the `temporary` @@ -102,8 +132,7 @@ impl Redirect { /// 301 and 302 codes, respectively. /// /// ``` - /// # use actix_web::http::StatusCode; - /// # use actix_web::web::Redirect; + /// # use actix_web::{http::StatusCode, web::Redirect}; /// // redirects would use "301 Moved Permanently" status code /// Redirect::new("/old", "/new") /// .using_status_code(StatusCode::MOVED_PERMANENTLY); @@ -140,7 +169,7 @@ impl Responder for Redirect { if let Ok(hdr_val) = self.to.parse() { res.headers_mut().insert(LOCATION, hdr_val); } else { - debug!( + log::error!( "redirect target location can not be converted to header value: {:?}", self.to ); diff --git a/actix-web/src/web.rs b/actix-web/src/web.rs index 6e6d77c10..0533f7f8f 100644 --- a/actix-web/src/web.rs +++ b/actix-web/src/web.rs @@ -11,8 +11,10 @@ //! - [`Bytes`]: Raw payload //! //! # Responders -//! - [`Json`]: JSON request payload -//! - [`Bytes`]: Raw request payload +//! - [`Json`]: JSON response +//! - [`Form`]: URL-encoded response +//! - [`Bytes`]: Raw bytes response +//! - [`Redirect`](Redirect::to): Convenient redirect responses use std::{borrow::Cow, future::Future}; @@ -46,6 +48,7 @@ pub use crate::types::*; /// For instance, to route `GET`-requests on any route matching `/users/{userid}/{friend}` and store /// `userid` and `friend` in the exposed `Path` object: /// +/// # Examples /// ``` /// use actix_web::{web, App, HttpResponse}; /// @@ -75,6 +78,7 @@ pub fn resource(path: T) -> Resource { /// - `/{project_id}/path2` /// - `/{project_id}/path3` /// +/// # Examples /// ``` /// use actix_web::{web, App, HttpResponse}; /// @@ -188,11 +192,13 @@ pub fn service(path: T) -> WebService { /// /// See [`Redirect`] docs for usage details. /// +/// # Examples /// ``` /// use actix_web::{web, App}; /// /// let app = App::new() -/// .service(web::redirect("/one", "/two")); +/// // the client will resolve this redirect to /api/to-path +/// .service(web::redirect("/api/from-path", "to-path")); /// ``` pub fn redirect( from: impl Into>,