How to Use Topcoat’s Cookie Jar API for Reading and Writing Cookies
Topcoat’s cookie jar API stores cookies in a request-scoped jar accessed via cookies(cx), allowing you to read incoming cookies with .get() and queue outgoing cookies with .add() or .remove(), which are automatically serialized into Set-Cookie headers when the response is sent.
The tokio-rs/topcoat framework provides a type-safe cookie jar API that eliminates manual header parsing by injecting a CookieJarCell into each request context. When enabled, the router layer automatically parses the incoming Cookie header once, memoizes it for the request duration, and flushes any pending changes to the response headers when the handler completes.
Enabling Cookie Support on the Router
Before handlers can access cookies, you must enable the cookie layer on your Router using the .cookies() builder method. This registers the internal CookieJarCell middleware that manages the request-scoped storage.
use topcoat::router::Router;
let router = Router::builder()
.cookies()
.build();
Once enabled, the router automatically calls write_cookies(cx, &mut headers) during response finalization to serialize any queued Set-Cookie entries—see the implementation in crates/topcoat-cookie/src/lib.rs at lines 101-115.
Reading Cookies from the Jar
Access the cookie jar within any handler by calling the cookies(cx) helper. This function extracts the CookieJarCell from the request context and, on first invocation, parses the Cookie header into a CookieJar structure that is memoized for subsequent access.
use topcoat::cookie::{cookies, Cookie};
#[route(GET "/preferences")]
async fn get_theme(cx: &topcoat::context::Cx) -> String {
let jar = cookies(cx);
match jar.get("theme") {
Some(cookie) => cookie.value().to_string(),
None => "default".to_string(),
}
}
The get(name) method returns Option<Cookie> if the cookie exists, or None if absent. This read-only operation does not produce any Set-Cookie output. The memoization logic ensures the header is parsed only once per request, as implemented in crates/topcoat-cookie/src/lib.rs at lines 36-44. The test reads_incoming_cookies at lines 73-81 demonstrates this behavior.
Writing and Removing Cookies
To set a cookie, use .add() with a Cookie builder or a (name, value) tuple. To delete a cookie, use .remove(). These methods queue entries that remain buffered until the router flushes them during response finalization.
use topcoat::cookie::{cookies, Cookie};
#[route(POST "/theme")]
async fn set_theme(cx: &topcoat::context::Cx) {
let jar = cookies(cx);
// Queue a Set-Cookie header with path attribute
jar.add(Cookie::build(("theme", "dark")).path("/").build());
// Remove the legacy cookie by expiring it
jar.remove(Cookie::build(("old_theme", "")).build());
}
According to the add_emits_set_cookie test in crates/topcoat-cookie/src/lib.rs at lines 103-110, calling .add() immediately schedules the cookie for emission, though the actual Set-Cookie headers are not appended to the response until the handler returns and the router invokes the finalization logic.
Composable Security and Attributes
Topcoat’s Cookies trait supports composable decorators that wrap the underlying jar to add signing, encryption, prefixes, and default attributes. You can stack these behaviors in any order using method chaining.
Available combinators include:
signed(&key)– Cryptographically signs cookies to prevent tamperingprivate(&key)– Encrypts cookie values for confidentialitydefault_secure(bool)– Sets theSecureattribute on all cookiesdefault_http_only(bool)– Sets theHttpOnlyattributedefault_same_site(SameSite)– Applies SameSite policy (Lax, Strict, None)override_prefix_host()– Adds the__Host-prefix for security
Access pre-configured variants via signed_cookies(cx) and private_cookies(cx), or build custom stacks:
use topcoat::cookie::{cookies, SameSite, Key};
// Create a helper that applies security defaults
fn secure_cookies(cx: &topcoat::context::Cx) -> impl topcoat::cookie::Cookies {
cookies(cx)
.default_secure(true)
.default_http_only(true)
.default_same_site(SameSite::Lax)
.default_path("/")
}
// Usage with signing
#[route(POST "/login")]
async fn login(cx: &topcoat::context::Cx) {
let key = Key::generate(); // Store securely in app context in production
let jar = cookies(cx).signed(&key);
jar.add(topcoat::cookie::cookie!("session_id" = "abc123"));
}
The trait definition and combinator implementations are located in crates/topcoat-cookie/src/lib.rs at lines 34-92. A complete example of stacking signing, prefixing, and default attributes appears in the documentation at crates/topcoat/docs/cookie.md lines 162-172.
Complete Working Example
This full example demonstrates router configuration, reading existing cookies, writing new ones with attributes, and using signed cookies:
use topcoat::{
cookie::{Cookies, Cookie, SameSite, cookies, signed_cookies},
context::Cx,
router::{Router, route},
};
#[tokio::main]
async fn main() {
let router = Router::builder()
.cookies()
.build();
// Router setup continues...
}
// Toggle theme preference
#[route(POST "/api/theme")]
async fn toggle_theme(cx: &Cx) -> topcoat::Result<String> {
let jar = cookies(cx);
// Read existing value
let next_theme = match jar.get("theme") {
Some(c) if c.value() == "dark" => "light",
_ => "dark",
};
// Write with specific attributes
jar.add(
Cookie::build(("theme", next_theme))
.path("/")
.same_site(SameSite::Lax)
.build()
);
Ok(next_theme.to_owned())
}
// Secure login with signed cookie
#[route(POST "/api/login")]
async fn login(cx: &Cx) -> topcoat::Result<&'static str> {
// In production, retrieve key from app context instead of generating
signed_cookies(cx).add(topcoat::cookie::cookie!("user_id" = "42"));
Ok("logged in")
}
Summary
- Enable cookies by calling
.cookies()on theRouterbuilder to activate the request-scopedCookieJarCell. - Read cookies using
cookies(cx).get(name), which returnsOption<Cookie>and memoizes the parsed header for the request duration. - Write cookies using
.add()or remove them using.remove(); changes are queued and automatically flushed toSet-Cookieheaders when the handler completes. - Secure cookies by chaining combinators like
.signed(&key),.private(&key), or setting default attributes (secure, http-only, SameSite) on the jar. - Access specialized jars via
signed_cookies(cx)andprivate_cookies(cx)for common security patterns.
Frequently Asked Questions
How do I access the cookie jar in a Topcoat handler?
Call the cookies(cx) function with the request context. This returns a type implementing the Cookies trait that provides methods to get, add, and remove cookies. The first call to cookies(cx) in a request triggers parsing of the Cookie header, with results cached in the CookieJarCell for subsequent access.
When are Set-Cookie headers actually written to the response?
The headers are written during the router's response finalization phase. After your handler returns, Topcoat automatically calls write_cookies(cx, &mut headers), which extracts pending changes from the jar and appends them as Set-Cookie headers to the outgoing response. You do not need to manually flush the jar.
Can I use both signed and unsigned cookies in the same request?
Yes. You can obtain multiple views of the same underlying jar with different decorators. For example, call cookies(cx) for standard cookies and signed_cookies(cx) for signed variants. Both operate on the same request-scoped storage but apply different transformations when serializing outgoing cookies.
What happens if I call cookies(cx) multiple times in one handler?
The function returns the same memoized CookieJar instance on every call within a single request. As implemented in crates/topcoat-cookie/src/lib.rs at lines 36-44, the CookieJarCell ensures the Cookie header is parsed exactly once, preventing redundant computation while maintaining consistency across your handler.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →