Docs

Status, redirects, and errors

Set status codes, return redirects, and render error pages.

Open Markdown

A Proa route is an Axum handler: return an Axum response to decide early, or RouteResponse to let the Proa finalizer render a page. This page covers status in the handler, rendered 404 pages, redirects, and framework errors.

Setting status in the handler

Return ordinary Axum responses when the handler can decide before rendering:

src/routes/product.rs
use axum::{
    extract::Path,
    http::StatusCode,
    response::{IntoResponse, Response},
};
use proa::axum::RouteResponse;

pub async fn product(Path(id): Path<String>) -> Response {
    let Some(product) = load_product(&id).await else {
        return (StatusCode::NOT_FOUND, "Not found").into_response();
    };

    RouteResponse::ssr(ProductPage { product }).into_response()
}

This keeps missing records, auth failures, and validation failures out of the render tree when they do not need a full page shell.

Some error states do need that shell, and those become real pages.

Rendering a 404 page

Render a full Proa document when the error state needs the site's layout, metadata, or island bootstrap. For a managed document, declare the selected Page and set its status in the head hook:

src/pages/not_found.rs
use http::StatusCode;
use proa::{ComponentRole, DataLoader, SyncHeadOperation, WebContext, WebRenderSync, WriteError};
use proa::html_sync;

pub struct NotFoundPage;

impl<L: DataLoader> WebRenderSync<L> for NotFoundPage {
    const ROLE: ComponentRole = ComponentRole::PAGE;

    fn render_head(&self, cx: &mut WebContext<L>) -> impl SyncHeadOperation {
        if !cx.response_mut().set_status(StatusCode::NOT_FOUND) {
            return Err(WriteError::render("response headers already committed"));
        }
        cx.head_mut().title("Page not found")
    }

    fn render(self, cx: &mut WebContext<L>) -> Result<(), WriteError> {
        html_sync! {
            <main>
                <h1>"Page not found"</h1>
                <p>"The page may have moved or no longer exists."</p>
            </main>
        }
        .render(cx)
    }
}

Compose it inside the RootDocument from Layouts and Pages, then return RouteResponse::ssr(RootDocument { children: NotFoundPage }). Enable managed metadata with FrameworkBuilder::with_head(...) or the response's with_head(...). This hook works for buffered and streamed documents; the status is decided before the Page body starts.

The same response context also stages headers and cookies.

Setting headers during render

Components stage response metadata through cx.response_mut():

src/pages/not_found.rs
use http::{header, HeaderValue, StatusCode};

cx.response_mut().set_status(StatusCode::CREATED);
cx.response_mut().insert_header(
    header::CACHE_CONTROL,
    HeaderValue::from_static("public, max-age=60"),
);
cx.response_mut().append_set_cookie(HeaderValue::from_static("theme=dark; Path=/; SameSite=Lax"));

set_status, insert_header, append_header, and append_set_cookie return bool. Treat false as a late mutation.

Late mutations are exactly what streaming makes possible, so streamed routes need a tighter rule.

Setting status on a streamed route

Set status and headers in the handler when the decision is already known:

src/pages/dashboard.rs
use axum::http::{header, StatusCode};

(
    StatusCode::OK,
    [(header::CACHE_CONTROL, "private, no-store")],
    RouteResponse::ssr_stream(DashboardPage),
)

For managed documents, layout and Page render_head hooks can also stage status, headers, and cookies before publication. Put data-dependent HTTP decisions there, before the selected Page body starts. Once the Page metadata is sealed and the head outlet is available, Proa publishes those decisions; can_mutate_headers() becomes false and later mutations are refused.

Manual streaming documents commit their response head before root rendering, so make their HTTP decisions in the handler. Buffered sync and async routes can stage response metadata until finalization. See Metadata and streaming.

Redirects avoid the problem entirely: they never enter the render tree.

Redirecting

Use Axum redirects for moved routes, auth gates, and canonical URLs:

src/routes/legacy.rs
use axum::response::Redirect;

pub async fn old_docs_path() -> Redirect {
    Redirect::permanent("/docs/framework")
}

When a handler may either redirect or render, return Response:

src/routes/account.rs
use axum::response::{IntoResponse, Redirect, Response};
use proa::axum::RouteResponse;

pub async fn account() -> Response {
    if !is_signed_in().await {
        return Redirect::to("/login").into_response();
    }

    RouteResponse::ssr(AccountPage).into_response()
}

Your own error types can carry the same decision, once you give them a status code.

Mapping application errors

Map application errors to explicit status codes. Keep sensitive details in logs, not response bodies:

src/error.rs
use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
};

pub enum AppError {
    NotFound,
    Unauthorized,
    Upstream,
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        match self {
            Self::NotFound => (StatusCode::NOT_FOUND, "Not found").into_response(),
            Self::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized").into_response(),
            Self::Upstream => (StatusCode::BAD_GATEWAY, "Temporary upstream error").into_response(),
        }
    }
}

Then use Result<Response, AppError> or Result<impl IntoResponse, AppError> in handlers.

Failures below your code, in render or transport, never reach an AppError.

Handling framework errors

The framework protects the response shape for infrastructure failures:

SourceResponse
Render, bootstrap, or invariant error500 with a terse body such as render error
Encoding error500 encoding error
Default request timeout408 Request Timeout
Rejected cross-origin unsafe browser request403 Forbidden
Missing embedded static asset404 Not Found

Use tracing for the detailed error path and return short public messages to users.

Checking the error paths

Before shipping:

Next steps

Search

Type at least 2 characters