diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index a018bc248..06a2cccc9 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -5,7 +5,9 @@ - Add `ContentDisposition::attachment` constructor. [#2867] - 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 `guard::Acceptable` for matching against `Accept` header mime types. [#2265] +[#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> {