From 5ee555462f54768a797e7af1548bc89be36d292f Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Wed, 19 Jan 2022 16:36:11 +0000
Subject: [PATCH] add `HttpResponse::add_removal_cookie` (#2586)

---
 CHANGES.md               |  4 +++
 src/response/response.rs | 73 +++++++++++++++++++++++++++++++++++-----
 2 files changed, 69 insertions(+), 8 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 31b17cba..fae67107 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 4aba4b62..33f0a54a 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<B = BoxBody> {
     res: Response<B>,
-    pub(crate) error: Option<Error>,
+    error: Option<Error>,
 }
 
 impl HttpResponse<BoxBody> {
@@ -116,18 +116,54 @@ impl<B> HttpResponse<B> {
         }
     }
 
-    /// 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<B> HttpResponse<B> {
         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()
+        );
+    }
+}