Skip to main content

yggdrasil/
request_ctx.rs

1//! 请求上下文访问:server fn 与服务端渲染期间读取当前请求、写入响应。
2//!
3//! `leptos_axum` 在处理 server fn 与渲染页面时,把 [`http::request::Parts`] 和
4//! [`leptos_axum::ResponseOptions`] 放进响应式上下文。本模块封装对它们的读取,
5//! 业务代码不直接依赖 `leptos_axum`。无请求上下文(后台任务、MCP 工具、单元测试)
6//! 时各函数返回 `None` 或空操作。
7
8#[cfg(all(feature = "server", test))]
9use std::future::Future;
10
11/// 当前请求的 [`http::request::Parts`](含请求头与扩展)。
12#[cfg(feature = "server")]
13pub fn request_parts() -> Option<http::request::Parts> {
14    leptos::prelude::use_context::<http::request::Parts>()
15}
16
17/// 当前请求的请求头副本。
18#[cfg(feature = "server")]
19pub fn request_headers() -> Option<http::HeaderMap> {
20    request_parts().map(|parts| parts.headers)
21}
22
23/// 读取中间件放进请求扩展的值(例如 `ShareContext`)。
24#[cfg(feature = "server")]
25pub fn request_extension<T: Clone + Send + Sync + 'static>() -> Option<T> {
26    request_parts().and_then(|parts| parts.extensions.get::<T>().cloned())
27}
28
29/// 追加一个响应头(例如登录时的 `Set-Cookie`);无响应上下文时忽略。
30#[cfg(feature = "server")]
31pub fn append_response_header(name: http::HeaderName, value: http::HeaderValue) {
32    if let Some(options) = leptos::prelude::use_context::<leptos_axum::ResponseOptions>() {
33        options.append_header(name, value);
34    }
35}
36
37/// 设置响应状态码。服务端写入 `ResponseOptions`,浏览器端空操作。
38///
39/// 只在首个响应块发出前有效:leptos 取到首块后才写入状态与响应头。`SsrMode::Async` 等全部渲染完
40/// 才出首块,状态码可靠。流式(InOrder)路由里 `<Suspense>` 内容即使不 await,服务端也要等后台
41/// Effect 确认没有挂起任务才输出,能否赶上首块取决于线程调度;前台页面都包在 FrontendLayout 的
42/// Suspense 里,所以需要 404/500 的前台路由必须用 Async。
43pub fn set_status(code: u16) {
44    #[cfg(feature = "server")]
45    if let Some(options) = leptos::prelude::use_context::<leptos_axum::ResponseOptions>() {
46        if let Ok(status) = http::StatusCode::from_u16(code) {
47            options.set_status(status);
48        }
49    }
50    #[cfg(not(feature = "server"))]
51    let _ = code;
52}
53
54/// 测试辅助:在带有请求上下文的作用域内执行异步代码,等价于 server fn 被调用时的环境。
55#[cfg(all(feature = "server", test))]
56pub async fn scope_with_request<F: Future>(parts: http::request::Parts, fut: F) -> F::Output {
57    use leptos::prelude::{provide_context, Owner};
58
59    let owner = Owner::new();
60    owner
61        .with(|| {
62            provide_context(parts);
63            provide_context(leptos_axum::ResponseOptions::default());
64            leptos::reactive::computed::ScopedFuture::new(fut)
65        })
66        .await
67}
68
69#[cfg(all(test, feature = "server"))]
70mod tests {
71    use super::*;
72
73    fn parts() -> http::request::Parts {
74        let mut request = http::Request::builder()
75            .header("cookie", "session=abc")
76            .body(())
77            .unwrap();
78        request.extensions_mut().insert(7_u32);
79        request.into_parts().0
80    }
81
82    #[tokio::test]
83    async fn scope_exposes_request_headers_and_extensions() {
84        let (cookie, extension, status_ok) = scope_with_request(parts(), async {
85            set_status(404);
86            let cookie = request_headers()
87                .and_then(|headers| headers.get("cookie").cloned())
88                .map(|value| value.to_str().unwrap().to_string());
89            let status = leptos::prelude::use_context::<leptos_axum::ResponseOptions>()
90                .map(|options| options.0.read().unwrap().status);
91            (
92                cookie,
93                request_extension::<u32>(),
94                status == Some(Some(http::StatusCode::NOT_FOUND)),
95            )
96        })
97        .await;
98        assert_eq!(cookie.as_deref(), Some("session=abc"));
99        assert_eq!(extension, Some(7));
100        assert!(status_ok);
101    }
102
103    #[tokio::test]
104    async fn outside_a_request_everything_is_empty() {
105        assert!(request_parts().is_none());
106        assert!(request_headers().is_none());
107        assert_eq!(request_extension::<u32>(), None);
108        set_status(500);
109    }
110}