pavex/cookie/
components.rs

1use crate::Response;
2use crate::cookie::ResponseCookies;
3use crate::cookie::errors::{ExtractRequestCookiesError, InjectResponseCookiesError};
4use crate::error::UnexpectedError;
5use crate::request::RequestHead;
6use biscotti::{Processor, RequestCookies};
7use http::HeaderValue;
8use http::header::{COOKIE, SET_COOKIE};
9use pavex_macros::{post_process, request_scoped};
10use tracing_log_error::log_error;
11
12/// Parse cookies out of the incoming request.
13///
14/// It's the default constructor for [`RequestCookies`].
15#[request_scoped(pavex = crate)]
16pub fn extract_request_cookies<'request>(
17    request_head: &'request RequestHead,
18    processor: &Processor,
19) -> Result<RequestCookies<'request>, ExtractRequestCookiesError> {
20    fn extract_request_cookie<'request>(
21        header: &'request HeaderValue,
22        processor: &Processor,
23        cookies: &mut RequestCookies<'request>,
24    ) -> Result<(), ExtractRequestCookiesError> {
25        let header = header
26            .to_str()
27            .map_err(ExtractRequestCookiesError::InvalidHeaderValue)?;
28        cookies.extend_from_header(header, processor).map_err(|e| {
29            use biscotti::errors::ParseError::*;
30            match e {
31                MissingPair(e) => ExtractRequestCookiesError::MissingPair(e),
32                EmptyName(e) => ExtractRequestCookiesError::EmptyName(e),
33                Crypto(e) => ExtractRequestCookiesError::Crypto(e),
34                Decoding(e) => ExtractRequestCookiesError::Decoding(e),
35                _ => ExtractRequestCookiesError::Unexpected(UnexpectedError::new(e)),
36            }
37        })?;
38        Ok(())
39    }
40
41    let mut cookies = RequestCookies::new();
42    for header in request_head.headers.get_all(COOKIE).into_iter() {
43        // Per RFC 6265 (HTTP State Management Mechanism), servers are free to ignore the Cookie
44        // header entirely (Section 4.2.2), and the spec places no requirement to reject requests
45        // containing malformed cookies. In practice, major frameworks (Tomcat, Django, Express, etc.)
46        // use tolerant, best-effort parsing to avoid breaking legitimate requests just because one
47        // cookie is corrupt, truncated, or non-compliant.
48        //
49        // We follow the same approach: skip only the invalid cookie(s) and keep the rest.
50        // This prevents unnecessary 4xx/5xx responses, avoids breaking user sessions due to
51        // transient client or proxy bugs, and mitigates the risk of denial-of-service from
52        // intentionally malformed Cookie headers.
53        if let Err(e) = extract_request_cookie(header, processor, &mut cookies) {
54            log_error!(
55                e,
56                level: tracing::Level::WARN,
57                "A request cookie is invalid, ignoring it"
58            );
59        }
60    }
61
62    Ok(cookies)
63}
64
65/// Attach cookies to the outgoing response.
66///
67/// It consumes [`ResponseCookies`] by value since no response cookies should be
68/// added after the execution of this middleware.
69#[post_process(pavex = crate)]
70pub fn inject_response_cookies(
71    mut response: Response,
72    response_cookies: ResponseCookies,
73    processor: &Processor,
74) -> Result<Response, InjectResponseCookiesError> {
75    for value in response_cookies.header_values(processor) {
76        let value = HeaderValue::from_str(&value).map_err(|_| InjectResponseCookiesError {
77            invalid_header_value: value,
78        })?;
79        response = response.append_header(SET_COOKIE, value);
80    }
81    Ok(response)
82}