axum_cloudflare_adapter/
lib.rs

1//! Axum Cloudflare Adapter
2//!
3//! A collection of tools allowing Axum to be run within a Cloudflare worker. See example usage below.
4//!
5
6//! ```
7//! use worker::*;
8//!
9//! use axum::{
10//! 		response::{Html},
11//! 		routing::get,
12//! 		Router as AxumRouter,
13//!         extract::State,
14//! };
15//! use axum_cloudflare_adapter::{to_axum_request, to_worker_response, wasm_compat, EnvWrapper};
16//! use tower_service::Service;
17//! use std::ops::Deref;
18//!
19//! #[derive(Clone)]
20//! pub struct AxumState {
21//!    	pub env_wrapper: EnvWrapper,
22//! }
23//!
24//! #[wasm_compat]
25//! async fn index(State(state): State<AxumState>) -> Html<&'static str> {
26//! 		let env: &Env = state.env_wrapper.env.deref();
27//! 		let worker_rs_version: Var = env.var("WORKERS_RS_VERSION").unwrap();
28//!         console_log!("WORKERS_RS_VERSION: {}", worker_rs_version.to_string());
29//!
30//! 		Html("<p>Hello from Axum!</p>")
31//! }
32//!
33//! #[event(fetch)]
34//! async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result<Response> {
35//!         let mut router: AxumRouter = AxumRouter::new()
36//! 				.route("/", get(index))
37//!                 .with_state(AxumState {
38//! 				    env_wrapper: EnvWrapper::new(env),
39//! 		        });
40//!
41//! 		let axum_request = to_axum_request(req).await.unwrap();
42//! 		let axum_response = router.call(axum_request).await.unwrap();
43//! 		let response = to_worker_response(axum_response).await.unwrap();
44//!
45//! 		Ok(response)
46//! }
47//!
48//! ```
49mod error;
50
51use axum::{
52    body::Body,
53    http::header::HeaderName,
54    http::{Method, Request, Uri},
55    response::Response,
56};
57pub use error::Error;
58use futures::TryStreamExt;
59use std::str::FromStr;
60use std::sync::Arc;
61use worker::{Headers, Request as WorkerRequest, Response as WorkerResponse};
62
63pub async fn to_axum_request(mut worker_request: WorkerRequest) -> Result<Request<Body>, Error> {
64    let method = Method::from_bytes(worker_request.method().to_string().as_bytes())?;
65
66    let uri = Uri::from_str(worker_request.url()?.to_string().as_str())?;
67
68    let body = worker_request.bytes().await?;
69
70    let mut http_request = Request::builder()
71        .method(method)
72        .uri(uri)
73        .body(Body::from(body))?;
74
75    for (header_name, header_value) in worker_request.headers() {
76        http_request.headers_mut().insert(
77            HeaderName::from_str(header_name.as_str())?,
78            header_value.parse()?,
79        );
80    }
81
82    Ok(http_request)
83}
84
85pub async fn to_worker_response(response: Response<Body>) -> Result<WorkerResponse, Error> {
86    let mut bytes: Vec<u8> = Vec::<u8>::new();
87
88    let (parts, body) = response.into_parts();
89
90    let mut stream = body.into_data_stream();
91    while let Some(chunk) = stream.try_next().await? {
92        bytes.extend_from_slice(&chunk);
93    }
94
95    let code = parts.status.as_u16();
96
97    let mut worker_response = WorkerResponse::from_bytes(bytes)?;
98    worker_response = worker_response.with_status(code);
99
100    let mut headers = Headers::new();
101    for (key, value) in parts.headers.iter() {
102        headers.set(key.as_str(), value.to_str()?).unwrap()
103    }
104    worker_response = worker_response.with_headers(headers);
105
106    Ok(worker_response)
107}
108
109pub use axum_wasm_macros::wasm_compat;
110
111#[derive(Clone)]
112pub struct EnvWrapper {
113    pub env: Arc<worker::Env>,
114}
115
116impl EnvWrapper {
117    pub fn new(env: worker::Env) -> Self {
118        Self { env: Arc::new(env) }
119    }
120}
121
122unsafe impl Send for EnvWrapper {}
123
124unsafe impl Sync for EnvWrapper {}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use axum::{response::Html, response::IntoResponse};
130    use wasm_bindgen_test::*;
131    use worker::{Method as WorkerMethod, RequestInit, ResponseBody};
132    wasm_bindgen_test_configure!(run_in_browser);
133
134    #[wasm_bindgen_test]
135    async fn it_should_convert_the_worker_request_to_an_axum_request() {
136        let mut request_init = RequestInit::new();
137        let mut headers = Headers::new();
138        headers.append("Content-Type", "text/html").unwrap();
139        headers.append("Cache-Control", "no-cache").unwrap();
140        request_init.with_headers(headers);
141        request_init.with_method(WorkerMethod::Get);
142        let worker_request =
143            WorkerRequest::new_with_init("https://logankeenan.com", &request_init).unwrap();
144
145        let request = to_axum_request(worker_request).await.unwrap();
146
147        assert_eq!(request.uri(), "https://logankeenan.com");
148        assert_eq!(request.method(), "GET");
149        assert_eq!(request.headers().get("Content-Type").unwrap(), "text/html");
150        assert_eq!(request.headers().get("Cache-Control").unwrap(), "no-cache");
151    }
152
153    #[wasm_bindgen_test]
154    async fn it_should_convert_the_worker_request_to_an_axum_request_with_a_body() {
155        let mut request_init = RequestInit::new();
156        request_init.with_body(Some("hello world!".into()));
157        request_init.with_method(WorkerMethod::Post);
158        let worker_request =
159            WorkerRequest::new_with_init("https://logankeenan.com", &request_init).unwrap();
160
161        let request = to_axum_request(worker_request).await.unwrap();
162
163        let mut bytes: Vec<u8> = Vec::<u8>::new();
164
165        let mut stream = request.into_body().into_data_stream();
166        while let Some(chunk) = stream.try_next().await.unwrap() {
167            bytes.extend_from_slice(&chunk);
168        }
169
170        assert_eq!(bytes.to_vec(), b"hello world!");
171    }
172
173    #[wasm_bindgen_test]
174    async fn it_should_convert_the_axum_response_to_a_worker_response() {
175        let response = Html::from("Hello World!").into_response();
176        let worker_response = to_worker_response(response).await.unwrap();
177
178        assert_eq!(worker_response.status_code(), 200);
179        assert_eq!(
180            worker_response
181                .headers()
182                .get("Content-Type")
183                .unwrap()
184                .unwrap(),
185            "text/html; charset=utf-8"
186        );
187        let body = match worker_response.body() {
188            ResponseBody::Body(body) => body.clone(),
189            _ => vec![],
190        };
191        assert_eq!(body, b"Hello World!");
192    }
193
194    #[wasm_bindgen_test]
195    async fn it_should_convert_the_axum_response_to_a_worker_response_with_an_empty_body() {
196        let body = Body::empty();
197        let response = Response::builder()
198            .status(200)
199            .header("Content-Type", "text/html")
200            .body(body)
201            .unwrap();
202
203        let worker_response = to_worker_response(response).await.unwrap();
204
205        assert_eq!(worker_response.status_code(), 200);
206        assert_eq!(
207            worker_response
208                .headers()
209                .get("Content-Type")
210                .unwrap()
211                .unwrap(),
212            "text/html"
213        );
214        let body = match worker_response.body() {
215            ResponseBody::Body(body) => body.clone(),
216            _ => b"should be empty".to_vec(),
217        };
218        assert_eq!(body.len(), 0);
219    }
220}