Skip to main content

try_render

Function try_render 

Source
pub fn try_render(
    config: &PageConfig<'_>,
    ctx: &RenderCtx,
) -> Result<PageResponse, RenderError>
Expand description

Render a page on the server: the page with its counts, or why not.

The bridge to 3.0.0’s render. Unlike the deprecated render_page, it never substitutes an empty shell for a failure: a missing module, a walk the module or data fails, an exceeded budget, an invalid policy extension, a failed nonce draw and a panic each come back as a RenderError with a stable code, the route and the component path. The page is written into its own buffer and returned whole, or not at all, so no partial page leaves. Forma logs none of these: the handler chooses the response. (A caught panic still runs the process’s panic hook first: the default hook prints it to stderr, and a reporting hook, such as an error tracker’s, records it.)

It always renders on the server: config.render_mode steers only the deprecated render_page. A route the build declares ssr: false is served by render_browser_shell.

use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use forma_server::{try_render, PageConfig, RenderCtx};

fn home(config: &PageConfig) -> Response {
    match try_render(config, &RenderCtx::new()) {
        Ok(page) => page.into_response(), // no-store, policy, headers
        Err(error) => {
            // Count it by code and route; serve the application's error page.
            eprintln!("{error}");
            StatusCode::INTERNAL_SERVER_ERROR.into_response()
        }
    }
}

§Threads and panics

The walk is synchronous work on the caller’s thread, and forma-server starts nothing and depends on no async runtime. At the measured 12.5 million nodes a second, a page past about 12,500 nodes holds the thread past a millisecond; RenderCounts reports each page’s nodes and walk time, so a handler whose counts show that can move the render into tokio’s blocking pool itself (owned inputs, since the closure must be 'static):

async fn heavy(
    manifest: Arc<AssetManifest>,
    module: Arc<IrModule>,
    slots: SlotData,
) -> Result<PageResponse, RenderError> {
    tokio::task::spawn_blocking(move || {
        let config = PageConfig {
            title: "Ledger",
            route_pattern: "/ledger",
            manifest: &manifest,
            config_script: None,
            config_json: None,
            body_class: None,
            personality_css: None,
            body_prefix: None,
            render_mode: RenderMode::Phase2SsrReconcile,
            ir_module: Some(&module),
            slots: Some(&slots),
        };
        try_render(&config, &RenderCtx::new())
    })
    .await
    .expect("the blocking task ran")
}

A panic inside the render is caught and returned as RenderError::Fault, which holds only while the application unwinds on panic, as both templates leave it. Under panic = "abort" a fault still ends the process with every request in flight.

Verified by: crates/forma-server/tests/try_render.rs > “a_panicking_component_returns_fault_the_handler_answers_500_and_the_next_request_renders”