Docs
Status, redirects, and errors
Set status codes, return redirects, and render error pages.
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:
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:
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():
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:
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:
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:
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:
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:
| Source | Response |
|---|---|
| Render, bootstrap, or invariant error | 500 with a terse body such as render error |
| Encoding error | 500 encoding error |
| Default request timeout | 408 Request Timeout |
| Rejected cross-origin unsafe browser request | 403 Forbidden |
| Missing embedded static asset | 404 Not Found |
Use tracing for the detailed error path and return short public messages to users.
Checking the error paths
Before shipping:
- Decide in the handler when data is missing before rendering.
- Render a full error page only when it needs the site shell.
- Set streaming HTTP decisions in the handler or, for managed documents, in a layout/Page head hook before publication.
- Check the
boolreturned from response-context mutations in streaming code. - Use Axum
Redirectfor redirects instead of rendering meta-refresh pages. - Test
404, redirect, validation, timeout, and upstream-error paths.
Next steps
- Route responses
- Return synchronous, buffered async, or streaming SSR with islands.
- Data loading and streaming
- Load request data, apply cache hints, and stream slow sections with Suspense.
- Forms and actions
- Handle submissions with Axum and render invalid field state back to the user.
- Testing
- Unit, snapshot, and integration tests for the paths above.