diff --git a/CHANGES.md b/CHANGES.md index 31b17cba8..fae671072 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,14 @@ # Changes ## Unreleased - 2021-xx-xx +### Added +- `HttpResponse::add_removal_cookie` [#2586] + ### Removed - `HttpRequest::req_data[_mut]()`; request-local data is still available through `.extensions()`. [#2585] [#2585]: https://github.com/actix/actix-web/pull/2585 +[#2586]: https://github.com/actix/actix-web/pull/2586 ## 4.0.0-beta.20 - 2022-01-14 diff --git a/src/response/response.rs b/src/response/response.rs index 4aba4b623..33f0a54a6 100644 --- a/src/response/response.rs +++ b/src/response/response.rs @@ -27,7 +27,7 @@ use crate::{error::Error, HttpRequest, HttpResponseBuilder, Responder}; /// An outgoing response. pub struct HttpResponse { res: Response, - pub(crate) error: Option, + error: Option, } impl HttpResponse { @@ -116,18 +116,54 @@ impl HttpResponse { } } - /// Add a cookie to this response + /// Add a cookie to this response. + /// + /// # Errors + /// Returns an error if the cookie results in a malformed `Set-Cookie` header. #[cfg(feature = "cookies")] pub fn add_cookie(&mut self, cookie: &Cookie<'_>) -> Result<(), HttpError> { HeaderValue::from_str(&cookie.to_string()) - .map(|c| { - self.headers_mut().append(header::SET_COOKIE, c); - }) - .map_err(|e| e.into()) + .map(|cookie| self.headers_mut().append(header::SET_COOKIE, cookie)) + .map_err(Into::into) } - /// Remove all cookies with the given name from this response. Returns - /// the number of cookies removed. + /// Add a "removal" cookie to the response that matches attributes of given cookie. + /// + /// This will cause browsers/clients to remove stored cookies with this name. + /// + /// The `Set-Cookie` header added to the response will have: + /// - name matching given cookie; + /// - domain matching given cookie; + /// - path matching given cookie; + /// - an empty value; + /// - a max-age of `0`; + /// - an expiration date far in the past. + /// + /// If the cookie you're trying to remove has an explicit path or domain set, those attributes + /// will need to be included in the cookie passed in here. + /// + /// # Errors + /// Returns an error if the given name results in a malformed `Set-Cookie` header. + #[cfg(feature = "cookies")] + pub fn add_removal_cookie(&mut self, cookie: &Cookie<'_>) -> Result<(), HttpError> { + let mut removal_cookie = cookie.to_owned(); + removal_cookie.make_removal(); + + HeaderValue::from_str(&removal_cookie.to_string()) + .map(|cookie| self.headers_mut().append(header::SET_COOKIE, cookie)) + .map_err(Into::into) + } + + /// Remove all cookies with the given name from this response. + /// + /// Returns the number of cookies removed. + /// + /// This method can _not_ cause a browser/client to delete any of its stored cookies. Its only + /// purpose is to delete cookies that were added to this response using [`add_cookie`] + /// and [`add_removal_cookie`]. Use [`add_removal_cookie`] to send a "removal" cookie. + /// + /// [`add_cookie`]: Self::add_cookie + /// [`add_removal_cookie`]: Self::add_removal_cookie #[cfg(feature = "cookies")] pub fn del_cookie(&mut self, name: &str) -> usize { let headers = self.headers_mut(); @@ -140,6 +176,7 @@ impl HttpResponse { headers.remove(header::SET_COOKIE); let mut count: usize = 0; + for v in vals { if let Ok(s) = v.to_str() { if let Ok(c) = Cookie::parse_encoded(s) { @@ -370,3 +407,23 @@ mod tests { assert!(dbg.contains("HttpResponse")); } } + +#[cfg(test)] +#[cfg(feature = "cookies")] +mod cookie_tests { + use super::*; + + #[test] + fn removal_cookies() { + let mut res = HttpResponse::Ok().finish(); + let cookie = Cookie::new("foo", ""); + res.add_removal_cookie(&cookie).unwrap(); + let set_cookie_hdr = res.headers().get(header::SET_COOKIE).unwrap(); + assert_eq!( + &set_cookie_hdr.as_bytes()[..25], + &b"foo=; Max-Age=0; Expires="[..], + "unexpected set-cookie value: {:?}", + set_cookie_hdr.to_str() + ); + } +}