Request-time state

Request-time state can be thought of as the next level up from build-time state, because it provides access to everything build state does, plus the actual HTTP request of the user who made the query. This allows you to generate user-specific state: for instance a custom dashboard could be prerendered for them based on their settings.

Here's an example of request state being used to show a user their own IP address (as their browser reports it, which can be spoofed):

use perseus::prelude::*;
use serde::{Deserialize, Serialize};
use sycamore::prelude::*;

#[derive(Serialize, Deserialize, Clone, ReactiveState)]
#[rx(alias = "PageStateRx")]
struct PageState {
    ip: String,
}

fn request_state_page<'a, G: Html>(cx: BoundedScope<'_, 'a>, state: &'a PageStateRx) -> View<G> {
    view! { cx,
        p {
            (
                format!("Your IP address is {}.", state.ip.get())
            )
        }
    }
}

pub fn get_template<G: Html>() -> Template<G> {
    Template::build("request_state")
        .request_state_fn(get_request_state)
        .view_with_state(request_state_page)
        .build()
}

// This returns a `Result<T, BlamedError<E>>` (or just `T`) because, obviously,
// it will be run at request-time: any errors could be a mising file (our
// fault), or a malformed cookie (the client's fault), etc., so we have to note
// the blame to get an accurate HTTP status code. This example is really
// infallible, but we've spelled it all out rather than using `T` so you can see
// how it works.
#[engine_only_fn]
async fn get_request_state(
    // We get all the same info as build state in here
    _info: StateGeneratorInfo<()>,
    // Unlike in build state, in request state we *also* get access to the information that the
    // user sent with their HTTP request IN this example, we extract the browser's reporting of
    // their IP address and display it to them
    req: Request,
) -> Result<PageState, BlamedError<std::convert::Infallible>> {
    Ok(PageState {
        ip: format!(
            "{:?}",
            req.headers()
                // NOTE: This header can be trivially spoofed, and may well not be the user's actual
                // IP address
                .get("X-Forwarded-For")
                .unwrap_or(&perseus::http::HeaderValue::from_str("hidden from view!").unwrap())
        ),
    })
}

Just as with a get_build_state function, this get_request_state function is async and returns a BlamedError<E>, where E is any compliant error type, but it could return the PageState directly if it wanted to. If you need a refresher on these properties, especially on error handling in these kinds of functions, see this page.

Importantly, request-time state gives us access to the usual StateGeneratorInfo, which contains the path, locale, and helper state, plus the Request, which is an HTTP request with the body stripped out. This is done because the body is meaningless in Perseus requests, and anything that needs it should use custom API endpoints.

In this example, we're using req to access the headers of the request, which are the main things you'll use (since these contain cookies, etc.). Here, we're just accessing the non-standard X-Forwarded-For header, which represents the IP address of the client in many cases.

Critically, the request provided to this function is not the 'real' request, meaning altering parts of it will have absolutely no effect whatsoever --- it's just a representation of it provided to your functions so they can access user details. For example, if you wanted to set headers, you should not add them here, but do this instead.

A request-time state generating function can be specified using .request_state_fn() on Template.