Skip to main content

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}