How to Handle Cookies (Including Signed and Encrypted) with Topcoat's Cookie Jar API

Use the cookies(cx) and cookies_mut(cx) helpers to access the request-scoped CookieJar, enabling the signed and private Cargo features for HMAC verification and encryption, and configure a SecretKey in the app context to automatically secure cookie values.

The tokio-rs/topcoat framework provides a request-scoped cookie jar that gives you ergonomic, type-safe access to HTTP cookies inside request handlers. Unlike traditional middleware-based approaches, Topcoat exposes cookie functionality through plain functions that accept the request context (&Cx), allowing you to read, write, sign, and encrypt cookies with minimal boilerplate. This article explains how to use the cookie jar API to handle standard, signed, and encrypted cookies based on the implementation in crates/topcoat/src/cookie.rs.

Topcoat stores the CookieJar inside the request context (Cx) and exposes it via two helper functions:

  • cookies(cx) – Returns a read-only reference to the jar for retrieving cookies.
  • cookies_mut(cx) – Returns a mutable reference for inserting or removing cookies.

Both functions are re-exported from crates/topcoat/src/lib.rs and provide immediate access without requiring middleware registration.

Reading Cookies

To retrieve a cookie by name, use the get method, which returns an Option<Cookie> with the URL-decoded value:

use topcoat::prelude::*;

async fn read_theme(cx: &Cx) -> Result<impl IntoResponse> {
    let theme = cookies(cx).get("theme")
        .map(|c| c.value().to_string())
        .unwrap_or_else(|| "light".to_string());
    
    Ok(format!("Current theme: {}", theme))
}

Writing and Removing Cookies

To set a cookie, construct a Cookie using the builder pattern and insert it into the mutable jar. You can configure standard attributes like path, domain, secure, http_only, and same_site:

async fn set_theme(cx: &Cx) -> Result<impl IntoResponse> {
    let mut jar = cookies_mut(cx);
    jar.insert(
        Cookie::build("theme", "dark")
            .http_only(true)
            .secure(true)
            .path("/")
            .max_age(time::Duration::days(7))
            .finish(),
    );
    
    Ok(StatusCode::OK)
}

To delete a cookie, call jar.remove(name), which sets an expiration date in the past.

Configuring Security Keys

Before using signed or encrypted cookies, you must register a 32-byte secret key in the application context. The SecretKey type is defined in crates/topcoat/src/crypto.rs and consumed by the cookie module.

Configure the key once during application startup:

use topcoat::crypto::SecretKey;

fn configure_app(app: &mut App) {
    let secret = SecretKey::new(b"very-secret-32-bytes-key!!");
    app.app_context(secret);
}

The cookie jar automatically retrieves this key from the context using app_context::<SecretKey>(cx) when signing or decrypting values.

Working with Signed Cookies

Enable the signed Cargo feature to add HMAC signature verification to your cookies. Signed cookies prevent tampering by appending a cryptographic signature to the value; if the client modifies the payload, verification fails.

Setting Signed Cookies

Use insert_signed to automatically sign the value before sending it to the client:

async fn set_csrf(cx: &Cx) -> Result<impl IntoResponse> {
    let token = generate_csrf_token();
    cookies_mut(cx).insert_signed("csrf_token", token);
    Ok(StatusCode::OK)
}

Verifying Signed Cookies

Use get_signed to retrieve and validate the signature in one operation. The method returns None if the cookie is missing or the signature is invalid:

async fn verify_csrf(cx: &Cx) -> Result<impl IntoResponse> {
    let token = cookies(cx).get_signed::<String>("csrf_token")
        .ok_or_else(|| Error::bad_request("Invalid or missing CSRF token"))?;
    
    // Proceed with token verification...
    Ok(StatusCode::OK)
}

The signed cookie implementation in crates/topcoat/src/cookie.rs handles the HMAC computation transparently, ensuring the signature is stripped from the returned value while validated on every read.

Working with Encrypted (Private) Cookies

Enable the private Cargo feature for encryption at rest. Private cookies encrypt the value using the secret key before base64-encoding it for transport, ensuring that users cannot inspect the cookie contents.

Setting Private Cookies

Use insert_private to encrypt sensitive data:

async fn login(cx: &Cx, user_id: u64) -> Result<impl IntoResponse> {
    cookies_mut(cx).insert_private("user_id", user_id);
    Ok(StatusCode::OK)
}

Reading Private Cookies

Use get_private to decrypt the value automatically:

async fn dashboard(cx: &Cx) -> Result<impl IntoResponse> {
    let user_id = cookies(cx).get_private::<u64>("user_id")
        .ok_or_else(|| Error::unauthorized("Session required"))?;
    
    Ok(format!("Welcome, user {}", user_id))
}

The encryption/decryption logic resides in the CookieJar implementation in crates/topcoat/src/cookie.rs, which uses the SecretKey configured in the app context.

Using Typed Cookies with CookieStore

For structured data, Topcoat provides CookieStore<T>, a generic wrapper that serializes any serde-compatible type to JSON before storage. This eliminates manual string parsing and provides compile-time type safety.

Define your data structure and use the typed store:

use topcoat::cookie::CookieStore;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Preferences {
    dark_mode: bool,
    language: String,
}

async fn save_prefs(cx: &Cx) -> Result<impl IntoResponse> {
    let store = CookieStore::<Preferences>::new("prefs");
    let prefs = Preferences {
        dark_mode: true,
        language: "en".to_string(),
    };
    
    // Automatically signs if the 'signed' feature is enabled
    store.set_signed(cx, &prefs)?;
    Ok(StatusCode::OK)
}

async fn load_prefs(cx: &Cx) -> Result<impl IntoResponse> {
    let store = CookieStore::<Preferences>::new("prefs");
    let prefs = store.get_signed(cx)?
        .unwrap_or_else(|| Preferences { dark_mode: false, language: "en".to_string() });
    
    Ok(Json(prefs))
}

The CookieStore<T> struct is defined in crates/topcoat/src/cookie.rs and delegates signing or encryption to the underlying CookieJar based on the enabled feature flags.

Summary

  • Access cookies via cookies(cx) for reading and cookies_mut(cx) for writing, both available anywhere you have &Cx.
  • Enable features by adding signed and/or private to your Cargo.toml to unlock get_signed/insert_signed and get_private/insert_private methods.
  • Configure keys once at startup using app.app_context(SecretKey::new(...)) from crates/topcoat/src/crypto.rs; the jar handles all cryptographic operations automatically.
  • Use typed storage with CookieStore<T> to serialize complex structures without manual JSON handling.
  • No middleware required—Topcoat's function-based design keeps cookie handling explicit and composable.

Frequently Asked Questions

How do I enable signed or encrypted cookies in Topcoat?

Add the corresponding feature flags to your Cargo.toml dependencies:

[dependencies]
topcoat = { version = "...", features = ["signed", "private"] }

Once enabled, the CookieJar methods insert_signed, get_signed, insert_private, and get_private become available. You must also configure a SecretKey via app.app_context() before handling requests.

The get_signed method returns None if the HMAC signature does not match the payload. This indicates either the cookie was modified by the client or the signing key has changed. Your handler should treat this as an invalid or missing cookie and respond appropriately (e.g., returning an error or resetting the session).

Can I use both signed and encrypted cookies in the same application?

Yes. You can enable both the signed and private features simultaneously. Use insert_signed for data that needs integrity but can be visible (like a user preference), and insert_private for sensitive data that must remain confidential (like user IDs or session tokens). Both methods share the same SecretKey configured in the app context.

Use the standard remove method on the mutable jar:

cookies_mut(cx).remove("session_id");

This works for all cookie types (plain, signed, or private) because it sets the cookie's expiration to the past, instructing the browser to delete it regardless of the payload format.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →