lilydocs
HTTP APIWebSocketConsumer
Security3 min read

Cookies

Read cookies, write them with a response, and choose signed or encrypted cookies.

RequestCookies reads incoming cookies. ResponseCookie defines an outgoing cookie's scope and browser policy. Use passthrough context to write a cookie alongside a typed response.

Read and write#

cookies.rs
use std::sync::Arc;
use lilyrs::http_api::{
    controller, Controller, ControllerInitError, ControllerTrait, CookieSameSite,
    Extensions, HttpApiError, PassthroughResponseContext, PassthroughResponseError,
    RequestCookies, ResponseCookie,
};
use serde::Serialize;

#[derive(Serialize)]
struct Preference { theme: String }

#[derive(Controller)]
#[base_path("/preferences")]
struct PreferenceController;

#[lilyrs::http_api::async_trait::async_trait]
impl ControllerTrait for PreferenceController {
    async fn new(_: Arc<Extensions>) -> Result<Self, ControllerInitError> { Ok(Self) }
}

#[controller]
impl PreferenceController {
    #[get("/theme")]
    async fn read(&self, cookies: RequestCookies) -> Result<Preference, HttpApiError> {
        let theme = cookies.get("theme")
            .map_err(|_| HttpApiError::BadRequest("cookie is invalid".into()))?
            .map(str::to_owned).unwrap_or_else(|| "light".into());
        Ok(Preference { theme })
    }

    #[put("/theme")]
    async fn set(&self, mut response: PassthroughResponseContext<'_>)
        -> Result<Preference, HttpApiError>
    {
        let cookie = ResponseCookie::new("theme", "dark")
            .map_err(PassthroughResponseError::from)?
            .http_only(true).secure(true).same_site(CookieSameSite::Lax)
            .path("/").map_err(PassthroughResponseError::from)?;
        response.set_cookie(&cookie)?;
        Ok(Preference { theme: "dark".into() })
    }
}

GET /preferences/theme returns light when no cookie exists, or the cookie's value when present. PUT /preferences/theme sets the theme cookie to dark and adds it to the JSON response. RequestCookies::get returns Result<Option<&str>, RequestCookieError>; convert a malformed cookie into a safe HTTP error.

HTTPS example. This code writes a Secure cookie. During local HTTP testing, the browser may not send it back; explicitly configure your development policy accordingly.

Signed cookies provide integrity checks; private cookies additionally provide confidentiality. Obtain keys from your application's secret source; do not hard-code production keys in examples.

With a secret and response context available
let key_ring = CookieKeyRing::new(primary_secret)?
    .with_previous(previous_secret)?;
let verified = cookies.private(&key_ring).get("session")?;
response.set_private_cookie(&cookie, &key_ring)?;

This is an API snippet: the application supplies primary_secret, previous_secret, cookie, and the response context; map error types into your action's contract. The SecureCookieValue inside verified provides value() and needs_rotation(). You can renew a value decoded with a previous key using the primary key.

CookieRemoval must repeat the original cookie's Path, Domain, and Partitioned scope. Deleting with a different scope may leave the actual browser cookie in place. Signed/private cookies have delete_signed_cookie and delete_private_cookie paths.

Session boundary#

A signed cookie does not prove that the session in the session store is still valid. Complete expiry, revocation, and refresh rotation policies in the session flow. For browser applications that carry identity through cookies, also choose a CSRF policy.

Sources for this page v0.1.0

This guide is based on the reviewed framework commit.

QUICK START
Documentation
Source code