dioxus_cloudflare/handler.rs
1//! Request handler that bridges Cloudflare Workers to the Dioxus Axum router.
2//!
3//! The [`handle`] function is the main entry point. It:
4//! 1. Stores the Worker `Env` and `Request` in thread-local context
5//! 2. Converts `worker::Request` → `http::Request`
6//! 3. Dispatches through the Dioxus Axum router (server function handlers)
7//! 4. Converts `http::Response` → `worker::Response`
8//! 5. Clears the thread-local context
9
10use std::convert::Infallible;
11
12use axum::body::Body;
13use http_body_util::BodyExt;
14use tower::ServiceExt;
15use worker::{Request, Response};
16
17use crate::context::{clear_context, set_context, take_cookies};
18
19/// Handle an incoming Cloudflare Worker request by dispatching it through
20/// the Dioxus server function router.
21///
22/// Call this from your Worker's `#[event(fetch)]` handler. All registered
23/// `#[server]` functions are automatically routed.
24///
25/// # Example
26///
27/// ```rust,ignore
28/// use worker::*;
29///
30/// extern "C" { fn __wasm_call_ctors(); }
31///
32/// #[event(fetch)]
33/// async fn fetch(req: Request, env: Env, _ctx: Context) -> Result<Response> {
34/// // Required: initialize inventory for server function registration.
35/// // SAFETY: Called once per Worker cold start. The `inventory` crate
36/// // requires this in WASM to register #[server] functions.
37/// unsafe { __wasm_call_ctors(); }
38///
39/// dioxus_cloudflare::handle(req, env).await
40/// }
41/// ```
42#[allow(clippy::missing_errors_doc)]
43pub async fn handle(req: Request, env: worker::Env) -> worker::Result<Response> {
44 // Store env + request in thread-local for cf::env() / cf::req()
45 let req_clone = req
46 .clone()
47 .map_err(|e| worker::Error::RustError(format!("request clone failed: {e}")))?;
48 set_context(env, req_clone);
49
50 // Convert worker::Request → http::Request
51 let http_req = worker_req_to_http(req).await?;
52
53 // Dispatch through the Dioxus Axum router
54 let http_resp = dispatch(http_req).await?;
55
56 // Convert http::Response → worker::Response
57 let mut worker_resp = http_to_worker_resp(http_resp).await?;
58
59 // Apply any cookies queued by cf::set_cookie() / cf::clear_cookie()
60 for cookie in take_cookies() {
61 worker_resp
62 .headers_mut()
63 .append("Set-Cookie", &cookie)
64 .map_err(|e| worker::Error::RustError(format!("cookie append failed: {e}")))?;
65 }
66
67 // Clean up thread-local context
68 clear_context();
69
70 Ok(worker_resp)
71}
72
73/// Convert a `worker::Request` into an `http::Request<Body>` that Axum
74/// can process.
75async fn worker_req_to_http(mut req: Request) -> worker::Result<http::Request<Body>> {
76 let method = match req.method() {
77 worker::Method::Get => http::Method::GET,
78 worker::Method::Post => http::Method::POST,
79 worker::Method::Put => http::Method::PUT,
80 worker::Method::Delete => http::Method::DELETE,
81 worker::Method::Options => http::Method::OPTIONS,
82 worker::Method::Head => http::Method::HEAD,
83 worker::Method::Patch => http::Method::PATCH,
84 _ => http::Method::GET,
85 };
86
87 let url = req.url()?;
88 let uri: http::Uri = url
89 .as_str()
90 .parse()
91 .map_err(|e| worker::Error::RustError(format!("invalid URI: {e}")))?;
92
93 let mut builder = http::Request::builder().method(method).uri(uri);
94
95 // Copy headers from worker request to http request
96 for (key, value) in req.headers() {
97 builder = builder.header(&key, &value);
98 }
99
100 // Read body as bytes and wrap in axum Body
101 let body_bytes = req.bytes().await?;
102
103 builder
104 .body(Body::from(body_bytes))
105 .map_err(|e| worker::Error::RustError(format!("request build failed: {e}")))
106}
107
108/// Build an Axum router containing all registered `#[server]` functions,
109/// then dispatch the request through it.
110///
111/// Uses `dioxus_server::ServerFunction::collect()` to iterate over all server
112/// functions registered via `inventory` (triggered by `__wasm_call_ctors`
113/// in the Worker entry point). Each function's path and method router are
114/// added to a `Router<FullstackState>`, which is then converted to
115/// `Router<()>` via `.with_state()`.
116async fn dispatch(req: http::Request<Body>) -> worker::Result<http::Response<Vec<u8>>> {
117 // Collect all #[server] functions registered by inventory and add
118 // them as routes to an Axum router.
119 //
120 // ServerFunction::collect() returns all functions that were registered
121 // when __wasm_call_ctors() ran. Each function knows its own path
122 // (e.g., "/api/ping") and HTTP method.
123 let mut router: axum::Router<dioxus_server::FullstackState> = axum::Router::new();
124 for func in dioxus_server::ServerFunction::collect() {
125 router = router.route(func.path(), func.method_router());
126 }
127
128 // Convert Router<FullstackState> → Router<()> using a headless state
129 // (no SSR renderer needed — we only serve API endpoints).
130 let router = router.with_state(dioxus_server::FullstackState::headless());
131
132 // Dispatch the request through the router. Router's Service impl has
133 // error type Infallible — it always produces a response (possibly 404).
134 let response = router
135 .oneshot(req)
136 .await
137 .unwrap_or_else(|e: Infallible| match e {});
138
139 // Read the response body into bytes
140 let (parts, body) = response.into_parts();
141 let body_bytes = body
142 .collect()
143 .await
144 .map_err(|e| worker::Error::RustError(format!("response body read failed: {e}")))?
145 .to_bytes()
146 .to_vec();
147
148 Ok(http::Response::from_parts(parts, body_bytes))
149}
150
151/// Convert an `http::Response<Vec<u8>>` back into a `worker::Response`.
152async fn http_to_worker_resp(resp: http::Response<Vec<u8>>) -> worker::Result<Response> {
153 let (parts, body) = resp.into_parts();
154
155 let mut worker_resp = if parts.status.is_success() {
156 Response::from_bytes(body)?
157 } else {
158 Response::error(
159 String::from_utf8_lossy(&body).into_owned(),
160 parts.status.as_u16(),
161 )?
162 };
163
164 // Copy response headers
165 for (key, value) in &parts.headers {
166 if let Ok(v) = value.to_str() {
167 worker_resp
168 .headers_mut()
169 .set(key.as_str(), v)
170 .map_err(|e| worker::Error::RustError(format!("header set failed: {e}")))?;
171 }
172 }
173
174 Ok(worker_resp)
175}