Leptos + Axum 服务端渲染错误处理实战:errors_axum 示例深度解析
【免费下载链接】leptosBuild fast web applications with Rust.项目地址: https://gitcode.com/GitHub_Trending/le/leptos
Leptos 作为使用 Rust 构建快速 Web 应用的全栈框架,其错误处理体系横跨客户端响应式系统与服务端 HTTP 层。本文以仓库内 errors_axum 示例 为骨架,完整讲解如何在 Axum 后端上让 Leptos 的ErrorBoundary、Errors集合、Server Function 错误与最终的 HTTP 状态码(404/500)协同工作,并给出可直接运行的启动命令、核心代码结构与源码级原理说明。读完本文,你将掌握一套在 SSR 场景下"界面展示错误 + 服务端返回正确状态码"的完整实践方案。
示例定位与整体思路
该示例的 README 只有一句话点题:"This example demonstrates how Leptos Errors can work with an Axum backend on a server",即演示Leptos 错误机制与 Axum 后端的服务端协作。它不是一个单纯渲染错误页的玩具,而是覆盖了三种典型错误来源:
- 路由级 404:访问不存在的路径(如
/404),由Routes的fallback兜底生成 404 错误; - 服务端函数错误:点击按钮触发一个必然失败的 Server Function(
cause_internal_server_error),演示ServerAction+ActionForm的错误提交路径; - 组件渲染错误:页面内某个组件直接返回
Err(...),由ErrorBoundary捕获,并使整个 SSR 页面产出 500 状态码。
README 还特别提示了一个可验证的亮点:"Can be used even when WASM is blocked"——错误处理在纯 SSR(未启用 WASM 水合)场景下依然有效,这正是服务端渲染错误体系的价值所在。
快速启动:两种运行方式
方式一:cargo-leptos(README 推荐)
README 的 Quick Start 只有一条命令:
cargo leptos watch它依赖项目根目录 Makefile.toml 中声明的构建配置。该示例的Cargo.toml中[package.metadata.leptos]段已经准备好全部参数:site-addr = "127.0.0.1:3000"、reload-port = 3001、bin-features = ["ssr"]、lib-features = ["hydrate"],因此直接运行即可在http://127.0.0.1:3000访问。默认地址见 Cargo.toml。
方式二:cargo-make(示例集通用方案)
按照 Examples README 的说明,也可以使用cargo-make运行(完全可选):
- 进入示例目录:
cd examples/errors_axum; - 安装
cargo-make:cargo install cargo-make; - 为当前工具链添加 WASM 目标:
rustup target add wasm32-unknown-unknown; - 运行
cargo make ci完成构建与测试; - 运行
cargo make start启动,随后按控制台输出的客户端地址访问; - 用
cargo make stop结束start启动的进程。
注意[package.metadata.leptos]中site-root = "target/site"的警告:该目录内容在重建时会被清空。示例的 Makefile.toml 通过extend引入../cargo-make/main.toml与../cargo-make/cargo-leptos.toml,并设置了CLIENT_PROCESS_NAME = "errors_axum"供构建流程识别客户端进程。
第一步:用 thiserror 定义领域错误类型
示例把错误类型集中放在 errors.rs:
use http::status::StatusCode; use thiserror::Error; #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum AppError { #[error("Not Found")] NotFound, #[error("Internal Server Error")] InternalServerError, } impl AppError { pub fn status_code(&self) -> StatusCode { match self { AppError::NotFound => StatusCode::NOT_FOUND, AppError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, } } }设计要点:
#[derive(Error)](thiserror):为每个变体提供Display文案("Not Found"、"Internal Server Error"),这些文案最终会渲染到错误页面;status_code()扩展方法:把领域错误映射为 HTTP 状态码。这是连接"应用层错误"与"HTTP 层状态码"的桥梁,后面ErrorTemplate在 SSR 阶段会调用它;Clone + PartialEq + Eq:便于在Memo中收集、比较错误值。
从源码结构看,这种"错误枚举 + 状态码映射"是 Leptos SSR 错误处理的标准切分方式:领域逻辑只关心"出了什么错",HTTP 语义由映射方法单独承担。
第二步:ErrorBoundary 与 Errors 集合的协作
Error与Errors的底层设计
在 Leptos 核心库 leptos/src/error_boundary.rs 中,Errors是一个透明包装FxHashMap<ErrorId, Error>的结构体,Error则是可向下转型(downcast)的错误特征对象。其关键方法:
pub struct Errors(FxHashMap<ErrorId, Error>); impl Errors { pub fn insert<E>(&mut self, key: ErrorId, error: E) where E: Into<Error>; pub fn insert_with_default_key<E>(&mut self, error: E) where E: Into<Error>; pub fn remove(&mut self, key: &ErrorId) -> Option<Error>; pub fn iter(&self) -> Iter<'_>; }insert_with_default_key专为"响应式系统之外"的错误设计(例如路由 fallback 中直接构造的错误),它使用默认的ErrorId作为键,而insert则接受响应式系统内throw_error产生的带 ID 错误。示例中两种用法都出现了。
路由 fallback:处理 404
在 landing.rs 的App组件中,Routes的fallback闭包构造了一个包含NotFound的Errors:
<Routes fallback=|| { let mut errors = Errors::default(); errors.insert_with_default_key(AppError::NotFound); view! { <ErrorTemplate errors/> } .into_view() }> <Route path=StaticSegment("") view=ExampleErrors/> </Routes>访问任意未注册路径(如/404)都会进入 fallback,通过insert_with_default_key注入AppError::NotFound,再交给<ErrorTemplate/>渲染。
组件内错误:ErrorBoundary 捕获
ExampleErrors页面内部放了一个必然出错的子组件:
#[component] pub fn ReturnsError() -> impl IntoView { Err::<String, AppError>(AppError::InternalServerError) }它直接返回Err,被外层包裹的ErrorBoundary捕获:
<ErrorBoundary fallback=|errors| view!{ <ErrorTemplate errors/>}> <ReturnsError/> </ErrorBoundary>注释还给出了一个可迁移的架构提示:ErrorBoundary 既可以放在 Router 上层做全局兜底,也可以下沉到具体路由内做局部兜底;页面上所有错误边界产生的错误都会汇总参与最终 SSR 状态码的决策。
ErrorTemplate:向下转型 + 渲染
error_template.rs 是所有错误的统一展示组件,它接收一个Signal<Errors>:
#[component] pub fn ErrorTemplate(#[prop(into)] errors: Signal<Errors>) -> impl IntoView { let errors = Memo::new(move |_| { errors .get_untracked() .into_iter() .filter_map(|(_, v)| v.downcast_ref::<AppError>().cloned()) .collect::<Vec<_>>() }); log!("Errors: {:#?}", &*errors.read_untracked()); ... }核心动作是downcast_ref::<AppError>():Errors中存放的是类型擦除的错误对象,必须向下转型回AppError才能读取status_code()与Display文案。若转型失败则被filter_map过滤掉(例如 Server Function 产生的ServerFnError不会出现在这里)。渲染部分会依据错误数量动态切换标题("Error"/"Errors"),并为每个错误输出<h2>状态码与<p>Error: 文案</p>。
第三步:SSR 阶段把错误写回 HTTP 状态码
客户端展示只解决"看得见",真正让浏览器网络面板出现 404/500 的是 SSR 阶段的ResponseOptions。同样在 error_template.rs:
#[cfg(feature = "ssr")] { let response = use_context::<ResponseOptions>(); if let Some(response) = response { response.set_status(errors.read_untracked()[0].status_code()); } }关键语义(源码注释明确说明):
ResponseOptions由leptos_axum在服务端渲染时注入上下文,通过use_context取出;set_status会覆盖Axum 默认的 200 状态码;- 只有第一个错误的响应码会被真正发送(
[0]),多个错误并存时的策略可由应用自行定制; - 该逻辑位于
#[cfg(feature = "ssr")]内,客户端水合时不会执行,因为浏览器端不关心 HTTP 状态码。
这解释了 README 中"WASM blocked 时依然可用"的原因:只要服务端完成了App的 SSR 渲染,ErrorBoundary/fallback 的错误路径就会执行,ResponseOptions会在响应构建阶段写入正确的状态码,与客户端是否水合无关。
第四步:Server Function 错误走 ActionForm
示例还演示了服务端函数错误的提交链路。在 landing.rs 中定义了一个必然失败的 Server Function:
#[server(CauseInternalServerError, "/api")] pub async fn cause_internal_server_error() -> Result<(), ServerFnError> { // fake API delay std::thread::sleep(std::time::Duration::from_millis(1250)); Err(ServerFnError::ServerError( "Generic Server Error".to_string(), )) }注意它使用了std::thread::sleep模拟 1.25 秒的 API 延迟,并在成功后返回ServerFnError::ServerError("Generic Server Error")——即该函数必然失败。
客户端侧通过ServerAction+ActionForm触发:
let generate_internal_error = ServerAction::<CauseInternalServerError>::new(); <ActionForm action=generate_internal_error> <input name="error1" type="submit" value="Generate Internal Server Error"/> </ActionForm>ServerAction会把 Server Function 包装为可提交的 action,ActionForm则以原生 HTML 表单方式 POST 到/api/cause_internal_server_error。这种"无 JS 也可提交"的表单设计,正是 README 强调"可再 WASM 被禁用时使用"的另一处体现——用户点击按钮后,即便没有水合脚本,请求依然能打到服务器并返回错误结果(可用浏览器网络面板观察该请求的响应状态)。
第五步:Axum 服务端装配
服务端入口在 main.rs,整体被#[cfg(feature = "ssr")]包裹,使用#[tokio::main]异步运行:
let conf = get_configuration(None).unwrap(); let leptos_options = conf.leptos_options; let addr = leptos_options.site_addr; let routes = generate_route_list(App); let app = Router::new() .route("/special/{id}", get(custom_handler)) .leptos_routes(&leptos_options, routes, { let leptos_options = leptos_options.clone(); move || shell(leptos_options.clone()) }) .fallback(leptos_axum::file_and_error_handler(shell)) .with_state(leptos_options); let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); axum::serve(listener, app.into_make_service()).await.unwrap();装配要点:
get_configuration(None)读取 cargo-leptos 注入的环境变量(None即使用 cargo-leptos 的环境变量),得到LeptosOptions;generate_route_list(App)从组件树中提取路由列表,交给.leptos_routes(...)注册 SSR 渲染路由;.fallback(leptos_axum::file_and_error_handler(shell))是错误处理的关键:所有未匹配的请求(包括/404这类不存在的路径)都会进入该 handler,由 Leptos 完成一次 SSR 渲染,渲染过程中 fallback 注入的NotFound错误会通过ResponseOptions把状态码改写为 404;- 示例还额外注册了一条
/special/{id}路由,使用自定义的custom_handler演示如何通过render_app_to_stream_with_context把 Axum 的State和Path参数以 context 形式注入到 Leptos 组件树中。
最后一段#[cfg(not(feature = "ssr"))]的main是个空实现,并注释说明:该示例无法编译成纯 CSR 的 Trunk 应用,因为"必须要有服务器才能演示错误状态码"——这再次印证了本示例的主题是服务端错误语义。
构建配置速览
Cargo.toml 中值得留意的配置:
- crate-type:
["cdylib", "rlib"],同时产出 WASM 绑定库与 Rust 库; - features:
hydrate = ["leptos/hydrate"]用于客户端水合;ssr按需引入axum、tower、tower-http、tokio、leptos_axum等依赖,保证纯客户端构建不携带服务端栈; [package.metadata.leptos]:output-name = "errors_axum"决定 WASM 产物名,style-file = "./style.css"指定样式入口(该示例的样式文件为 style.css),assets-dir = "public"会把 public 目录内容复制到站点根目录;bin-features = ["ssr"]/lib-features = ["hydrate"]:二进制目标只启用 SSR,库目标只启用水合,二者各自关闭默认特性,是 Leptos 全栈项目的标准拆分。
完整错误流复盘与验证方法
把上述五个部分串起来,一次"访问不存在页面"的完整链路是:
- 浏览器请求
/404; - Axum 路由未命中,进入
file_and_error_handler; - Leptos 服务端渲染
App,Routes的fallback构造Errors{NotFound}; ErrorTemplate通过downcast_ref::<AppError>()还原错误,渲染 404 页面;- 同一组件内通过
ResponseOptions.set_status(404)覆盖响应码; - 浏览器收到 404 状态码与错误页 HTML(即便未水合,页面与状态码依然正确)。
README 给出的验证方式很具体:点击/404链接、"same link in a new tab" 新标签页链接,以及触发 Server Error 按钮后,使用浏览器开发者工具(dev tools)的网络面板检查响应状态码。页面上的ReturnsError所在<div>会始终渲染ErrorTemplate,使该页面在 SSR 时恒为 500——打开首页的 Network 面板即可直接确认。
建议按以下顺序动手实验,逐项核对预期结果:
| 操作 | 预期结果 |
|---|---|
打开首页http://127.0.0.1:3000/ | 网络面板显示 200,页面底部 div 内渲染 500 错误模板 |
点击/404链接(同页或新标签) | 页面显示404 Not Found,网络面板状态码为 404 |
| 点击 "Generate Internal Server Error" 按钮 | 网络面板出现发往/api/...的 POST 请求,返回服务端错误 |
| 临时禁用 WASM 后重复上述操作 | 错误页与状态码行为不变(SSR 兜底生效) |
小结
errors_axum 示例用最小代码量覆盖了 Leptos 全栈错误处理的四个层次:领域错误建模(thiserror + 状态码映射)、响应式错误捕获(Errors/ErrorBoundary/ErrorTemplate)、服务端状态码回写(ResponseOptions)、Axum 装配兜底(file_and_error_handler)。对想要把"错误页 + 正确 HTTP 语义"落到生产项目的开发者而言,这是一个可以直接照搬的参考模板:扩展AppError枚举、在ErrorTemplate中补充更丰富的展示逻辑、并按需在组件树不同层级安放ErrorBoundary即可。
【免费下载链接】leptosBuild fast web applications with Rust.项目地址: https://gitcode.com/GitHub_Trending/le/leptos
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考