Browse documentation
Documentation

Request context

Edit this guide on GitHub

Cx gives handlers and components access to request data and shared application values.

Add cx: &Cx to a handler or component's parameters when it needs context. Hearth supplies it automatically.

Router request helpers

The router::request module exposes small functions for reading HTTP request data from cx.

use hearth::{
    context::Cx,
    router::request::{headers, method, uri},
};

fn request_summary(cx: &Cx) -> String {
    let user_agent = headers(cx)
        .get("user-agent")
        .and_then(|value| value.to_str().ok())
        .unwrap_or("unknown");

    format!("{} {} from {user_agent}", method(cx), uri(cx).path())
}

Use a field helper such as headers(cx) for one part of the request. See router::request for the available helpers.

Use parts(cx) when you need several fields at once:

use hearth::{context::Cx, router::request::parts};

fn cache_key(cx: &Cx) -> String {
    let parts = parts(cx);
    format!("{}:{}", parts.method, parts.uri)
}

Use extensions(cx) for typed request values attached by a lower-level request layer or service integration:

use hearth::{context::Cx, router::request::extensions};

struct RequestId(String);

fn request_id(cx: &Cx) -> Option<&str> {
    extensions(cx).get::<RequestId>().map(|id| id.0.as_str())
}

A handler reached through a rewrite sees the rewritten request in the HTTP field helpers. Those helpers have original_ counterparts returning the request as the client sent it, from original_parts(cx) down to original_uri(cx). Layers can also change the current request without a rewrite. For example, StripPrefixLayer changes uri(cx) while original_uri(cx) keeps the incoming URI. The client's IP address is resolved when the request arrives and stays the same across rewrites.

Path and query helpers

The module_param! macro and #[query_params] attribute declare typed values that you read with the path_param::<T>(cx) and query_params::<T>(cx) functions. Hearth parses typed path parameters and query structs lazily and memoizes them for the request.

// src/app/posts/post_id.rs
use hearth::{
    Result,
    context::Cx,
    router::{module_param, page, path_param, query_params},
    view::{View, view},
};

module_param!(post_id: uuid::Uuid, error = bad_request);

#[query_params(error = bad_request)]
struct PostQuery {
    preview: Option<bool>,
}

#[page]
async fn post(cx: &Cx) -> Result<impl View> {
    let post_id = path_param::<PostId>(cx)?;
    let query = query_params::<PostQuery>(cx)?;

    Ok(view! {
        <article data-preview=(query.preview.unwrap_or(false))>
            "post id: " (post_id.to_string())
        </article>
    })
}

Any function with &Cx can read these values.

App and request context helpers

This module exposes typed context accessors:

use hearth::context::{Cx, app_context};

fn db(cx: &Cx) -> &Database {
    app_context::<Database>(cx)
}

Values are keyed by Rust type. The required helpers panic when the requested type was not registered, so they are best wrapped in small application-specific functions like db(cx), config(cx), or current_tenant(cx).

Use the try_ helpers when a value is intentionally optional on some requests:

use hearth::context::{Cx, try_request_context};

fn current_customer(cx: &Cx) -> Option<&Customer> {
    try_request_context(cx)
}

Registering request context

Cx::with creates a child context with an additional value. Cx::with_many adds several values at once. The child inherits other context values and shares the request's state with its parent.

use hearth::context::{Cx, request_context};

struct Customer {
    name: String,
}

fn greet(cx: &Cx) -> String {
    let cx = cx.with(Customer {
        name: "Ada".to_owned(),
    });

    let customer: &Customer = request_context(&cx);
    format!("Hello, {}", customer.name)
}

Adding a type that is already present replaces it in the child scope. The parent still sees the original value. A router layer can pass a child context to the next handler to make its values available there.

Work that outlives the handler

A Cx is a handle to state shared by everything serving one request. The router drops its own handle once the response is sent, so a streaming response body or a spawned task cannot borrow the cx the handler was called with. Clone the Cx and move the owned handle into the work instead; it reads the same app and request context.

// src/app/orders.rs
use hearth::{
    Result,
    context::{Cx, request_context},
    router::route,
};

struct Customer {
    name: String,
}

#[route(POST)]
async fn place_order(cx: &Cx) -> Result<&'static str> {
    let cx = cx.clone();
    tokio::spawn(async move {
        let customer: &Customer = request_context(&cx);
        record(&customer.name).await;
    });
    Ok("queued")
}

A cloned handle can still read context after the handler returns. It cannot change response headers that have already been sent. Cookie writes after the jar is sealed panic.

Memoization

#[memoize] caches a function's result for one request. Repeated calls with the same arguments share the result when they see the same request context dependencies. Use it for expensive work needed in several places. See the macro's documentation for requirements and scoping rules.

Composing helpers

Any helper can accept cx: &``Cx, call other helpers, and return a domain-specific result:

use hearth::{
    context::Cx,
    router::request::{headers, uri},
};

fn locale(cx: &Cx) -> &str {
    headers(cx)
        .get("accept-language")
        .and_then(|value| value.to_str().ok())
        .and_then(|value| value.split(',').next())
        .unwrap_or("en")
}

fn canonical_url(cx: &Cx) -> String {
    format!("https://example.com{}", uri(cx).path())
}

That keeps pages, layouts, components, and routes focused on rendering or responding while shared request reads stay in ordinary Rust functions.