feat(web): add `cookie_raw` API (#4013)

This commit is contained in:
Yuki Okushi 2026-04-11 07:36:40 +09:00 committed by GitHub
parent ad7cbef1f9
commit 3556ae0b4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 102 additions and 5 deletions

View File

@ -2,6 +2,7 @@
## Unreleased
- Add `HttpRequest::{cookies_raw,cookie_raw}` and `ServiceRequest::{cookies_raw,cookie_raw}` for reading request cookies without percent-decoding names and values. [#3542]
- Enable dual-stack IPv6 sockets on Windows when possible so that Actix-created listeners bound to `[::]` also accept IPv4 connections.
- Panic when calling `Route::to()` or `Route::service()` after `Route::wrap()` to prevent silently dropping route middleware. [#3944]
- Fix `HttpRequest::{match_pattern,match_name}` reporting path-only matches when route guards disambiguate overlapping resources. [#3346]
@ -9,6 +10,7 @@
[#3944]: https://github.com/actix/actix-web/pull/3944
[#3346]: https://github.com/actix/actix-web/issues/3346
[#3542]: https://github.com/actix/actix-web/issues/3542
## 4.13.0

View File

@ -29,6 +29,9 @@ use crate::{
#[cfg(feature = "cookies")]
struct Cookies(Vec<Cookie<'static>>);
#[cfg(feature = "cookies")]
struct RawCookies(Vec<Cookie<'static>>);
/// An incoming request.
#[derive(Clone)]
pub struct HttpRequest {
@ -457,6 +460,8 @@ impl HttpRequest {
/// Load request cookies.
///
/// The names and values of cookies are percent-decoded.
///
/// Any cookie that cannot be parsed is omitted from the result.
/// This includes cookies with an empty name (e.g. `document.cookie = "=value"`).
#[cfg(feature = "cookies")]
@ -481,16 +486,49 @@ impl HttpRequest {
}))
}
/// Load request cookies **without** percent-decoding their names and values.
///
/// Any cookie that cannot be parsed is omitted from the result.
/// This includes cookies with an empty name (e.g. `document.cookie = "=value"`).
#[cfg(feature = "cookies")]
pub fn cookies_raw(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> {
use actix_http::header::COOKIE;
if self.extensions().get::<RawCookies>().is_none() {
let mut cookies = Vec::new();
for hdr in self.headers().get_all(COOKIE) {
let s = str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?;
for cookie_str in s.split(';').map(|s| s.trim()).filter(|s| !s.is_empty()) {
if let Ok(cookie) = Cookie::parse(cookie_str) {
cookies.push(cookie.into_owned());
}
}
}
self.extensions_mut().insert(RawCookies(cookies));
}
Ok(Ref::map(self.extensions(), |ext| {
&ext.get::<RawCookies>().unwrap().0
}))
}
/// Return request cookie.
#[cfg(feature = "cookies")]
pub fn cookie(&self, name: &str) -> Option<Cookie<'static>> {
if let Ok(cookies) = self.cookies() {
for cookie in cookies.iter() {
if cookie.name() == name {
return Some(cookie.to_owned());
}
}
return cookies.iter().find(|cookie| cookie.name() == name).cloned();
}
None
}
/// Return request cookie **without** percent-decoding its name and value.
#[cfg(feature = "cookies")]
pub fn cookie_raw(&self, name: &str) -> Option<Cookie<'static>> {
if let Ok(cookies) = self.cookies_raw() {
return cookies.iter().find(|cookie| cookie.name() == name).cloned();
}
None
}
}
@ -723,6 +761,49 @@ mod tests {
assert!(cookie.is_none());
}
#[test]
#[cfg(feature = "cookies")]
fn test_request_cookies_raw() {
let req = TestRequest::default()
.append_header((header::COOKIE, "cookie1=hello%20world"))
.append_header((header::COOKIE, "cookie2=%db"))
.to_http_request();
{
let cookies = req.cookies_raw().unwrap();
assert_eq!(cookies.len(), 2);
assert_eq!(cookies[0].name(), "cookie1");
assert_eq!(cookies[0].value(), "hello%20world");
assert_eq!(cookies[1].name(), "cookie2");
assert_eq!(cookies[1].value(), "%db");
}
let cookie = req.cookie_raw("cookie1");
assert!(cookie.is_some());
let cookie = cookie.unwrap();
assert_eq!(cookie.name(), "cookie1");
assert_eq!(cookie.value(), "hello%20world");
let cookie = req.cookie_raw("cookie2");
assert!(cookie.is_some());
let cookie = cookie.unwrap();
assert_eq!(cookie.name(), "cookie2");
assert_eq!(cookie.value(), "%db");
}
#[test]
#[cfg(feature = "cookies")]
fn test_request_cookies_raw_is_independent_from_encoded_cookies() {
let req = TestRequest::default()
.append_header((header::COOKIE, "cookie=%20"))
.to_http_request();
let cookie = req.cookie("cookie").unwrap();
assert_eq!(cookie.value(), " ");
let raw_cookie = req.cookie_raw("cookie").unwrap();
assert_eq!(raw_cookie.value(), "%20");
}
#[test]
#[cfg(feature = "cookies")]
fn test_empty_key() {

View File

@ -297,6 +297,13 @@ impl ServiceRequest {
self.req.cookies()
}
/// Return request cookies **without** percent-decoding their names and values.
#[cfg(feature = "cookies")]
#[inline]
pub fn cookies_raw(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> {
self.req.cookies_raw()
}
/// Return request cookie.
#[cfg(feature = "cookies")]
#[inline]
@ -304,6 +311,13 @@ impl ServiceRequest {
self.req.cookie(name)
}
/// Return request cookie **without** percent-decoding its name and value.
#[cfg(feature = "cookies")]
#[inline]
pub fn cookie_raw(&self, name: &str) -> Option<Cookie<'static>> {
self.req.cookie_raw(name)
}
/// Set request payload.
#[inline]
pub fn set_payload(&mut self, payload: Payload) {