1use futures::{SinkExt,Stream,StreamExt};
2use leptos::{provide_context, use_context, LeptosOptions, RuntimeId};
3use leptos_router::RouteListing;
4use route_table::RouteMatch;
5use spin_sdk::http::{Headers, IncomingRequest, OutgoingResponse, ResponseOutparam};
6pub mod request;
7pub mod request_parts;
8pub mod response;
9pub mod response_options;
10pub mod route_table;
11pub mod server_fn;
12
13use crate::server_fn::handle_server_fns_with_context;
14pub use request_parts::RequestParts;
15pub use response_options::ResponseOptions;
16pub use route_table::RouteTable;
17use leptos::server_fn::redirect::REDIRECT_HEADER;
18
19pub async fn render_best_match_to_stream<IV>(
20 req: IncomingRequest,
21 resp_out: ResponseOutparam,
22 routes: &RouteTable,
23 app_fn: impl Fn() -> IV + 'static + Clone,
24 leptos_opts: &LeptosOptions,
25) where
26 IV: leptos::IntoView + 'static,
27{
28 render_best_match_to_stream_with_context(req, resp_out, routes, app_fn, || {}, leptos_opts).await;
29}
30pub async fn render_best_match_to_stream_with_context<IV>(
31 req: IncomingRequest,
32 resp_out: ResponseOutparam,
33 routes: &RouteTable,
34 app_fn: impl Fn() -> IV + 'static + Clone,
35 additional_context: impl Fn() + Clone + Send + 'static,
36 leptos_opts: &LeptosOptions,
37) where
38 IV: leptos::IntoView + 'static,
39{
40 let url = url::Url::parse(&url(&req)).unwrap();
42 let path = url.path();
43
44 match routes.best_match(path) {
45 RouteMatch::Route(best_listing) => {
46 render_route_with_context(url, req, resp_out, app_fn, additional_context, leptos_opts, &best_listing).await
47 }
48 RouteMatch::ServerFn => handle_server_fns_with_context(req, resp_out, additional_context).await,
49 RouteMatch::None => {
50 eprintln!("No route found for {url}");
51 not_found(resp_out).await
52 }
53 }
54}
55
56async fn render_route<IV>(
57 url: url::Url,
58 req: IncomingRequest,
59 resp_out: ResponseOutparam,
60 app_fn: impl Fn() -> IV + 'static + Clone,
61 leptos_opts: &LeptosOptions,
62 listing: &RouteListing,
63) where
64 IV: leptos::IntoView + 'static,
65{
66 render_route_with_context(url, req, resp_out, app_fn, ||{}, leptos_opts, listing).await;
67}
68
69async fn render_route_with_context<IV>(
70 url: url::Url,
71 req: IncomingRequest,
72 resp_out: ResponseOutparam,
73 app_fn: impl Fn() -> IV + 'static + Clone,
74 additional_context: impl Fn() + Clone + Send + 'static,
75 leptos_opts: &LeptosOptions,
76 listing: &RouteListing,
77) where
78 IV: leptos::IntoView + 'static,
79{
80 if listing.static_mode().is_some() {
81 log_and_server_error("Static routes are not supported on Spin", resp_out);
82 return;
83 }
84
85 match listing.mode() {
86 leptos_router::SsrMode::OutOfOrder => {
87 let resp_opts = ResponseOptions::default();
88 let req_parts = RequestParts::new_from_req(&req);
89
90 let app = {
91 let app_fn2 = app_fn.clone();
92 let res_options = resp_opts.clone();
93 move || {
94 provide_contexts(&url, req_parts, res_options, additional_context);
95 (app_fn2)().into_view()
96 }
97 };
98 render_view_into_response_stm(app, resp_opts, leptos_opts, resp_out).await;
99 }
100 leptos_router::SsrMode::Async => {
101 let resp_opts = ResponseOptions::default();
102 let req_parts = RequestParts::new_from_req(&req);
103
104 let app = {
105 let app_fn2 = app_fn.clone();
106 let res_options = resp_opts.clone();
107 move || {
108 provide_contexts(&url, req_parts, res_options, additional_context);
109 (app_fn2)().into_view()
110 }
111 };
112 render_view_into_response_stm_async_mode(app, resp_opts, leptos_opts, resp_out).await;
113 }
114 leptos_router::SsrMode::InOrder => {
115 let resp_opts = ResponseOptions::default();
116 let req_parts = RequestParts::new_from_req(&req);
117
118 let app = {
119 let app_fn2 = app_fn.clone();
120 let res_options = resp_opts.clone();
121 move || {
122 provide_contexts(&url, req_parts, res_options, additional_context);
123 (app_fn2)().into_view()
124 }
125 };
126 render_view_into_response_stm_in_order_mode(app, leptos_opts, resp_opts, resp_out)
127 .await;
128 }
129 leptos_router::SsrMode::PartiallyBlocked => {
130 let resp_opts = ResponseOptions::default();
131 let req_parts = RequestParts::new_from_req(&req);
132
133 let app = {
134 let app_fn2 = app_fn.clone();
135 let res_options = resp_opts.clone();
136 move || {
137
138 provide_contexts(&url, req_parts, res_options, additional_context);
139 (app_fn2)().into_view()
140 }
141 };
142 render_view_into_response_stm_partially_blocked_mode(
143 app,
144 leptos_opts,
145 resp_opts,
146 resp_out,
147 )
148 .await;
149 }
150 }
151}
152
153async fn not_found(resp_out: ResponseOutparam) {
156 let og = OutgoingResponse::new(Headers::new());
157 og.set_status_code(404).expect("Failed to set status'");
158 resp_out.set(og);
159}
160
161async fn render_view_into_response_stm(
162 app: impl FnOnce() -> leptos::View + 'static,
163 resp_opts: ResponseOptions,
164 leptos_opts: &leptos::leptos_config::LeptosOptions,
165 resp_out: ResponseOutparam,
166) {
167 let (stm, runtime) = leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
168 app,
169 || leptos_meta::generate_head_metadata_separated().1.into(),
170 || {},
171 false);
172 build_stream_response(stm, leptos_opts, resp_opts, resp_out, runtime).await;
173}
174
175async fn render_view_into_response_stm_async_mode(
176 app: impl FnOnce() -> leptos::View + 'static,
177 resp_opts: ResponseOptions,
178 leptos_opts: &leptos::leptos_config::LeptosOptions,
179 resp_out: ResponseOutparam,
180) {
181 let (stm, runtime) = leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
185 app,
186 move || "".into(),
187 || {},
188 );
189 let html = leptos_integration_utils::build_async_response(stm, leptos_opts, runtime).await;
190
191 let status_code = resp_opts.status().unwrap_or(200);
192 let headers = resp_opts.headers();
193
194 let og = OutgoingResponse::new(headers);
195 og.set_status_code(status_code).expect("Failed to set status");
196 let mut ogbod = og.take_body();
197 resp_out.set(og);
198 ogbod.send(html.into_bytes()).await.unwrap();
199}
200
201async fn render_view_into_response_stm_in_order_mode(
202 app: impl FnOnce() -> leptos::View + 'static,
203 leptos_opts: &LeptosOptions,
204 resp_opts: ResponseOptions,
205 resp_out: ResponseOutparam,
206) {
207 let (stm, runtime) = leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
208 app,
209 || leptos_meta::generate_head_metadata_separated().1.into(),
210 || {},
211 );
212
213 build_stream_response(stm, leptos_opts, resp_opts, resp_out, runtime).await;
214}
215
216async fn render_view_into_response_stm_partially_blocked_mode(
217 app: impl FnOnce() -> leptos::View + 'static,
218 leptos_opts: &LeptosOptions,
219 resp_opts: ResponseOptions,
220 resp_out: ResponseOutparam,
221) {
222 let (stm, runtime) =
223 leptos::ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
224 app,
225 move || leptos_meta::generate_head_metadata_separated().1.into(),
226 || (),
227 true,
228 );
229 build_stream_response(stm, leptos_opts, resp_opts, resp_out, runtime).await;
230}
231
232async fn build_stream_response(
233 stm: impl Stream<Item = String>,
234 leptos_opts: &LeptosOptions,
235 resp_opts: ResponseOptions,
236 resp_out: ResponseOutparam,
237 runtime: RuntimeId,
238) {
239 let mut stm2 = Box::pin(stm);
240
241 let first_app_chunk = stm2.next().await.unwrap_or_default();
242 let (head, tail) = leptos_integration_utils::html_parts_separated(
243 leptos_opts,
244 leptos::use_context::<leptos_meta::MetaContext>().as_ref(),
245 );
246
247 let mut stm3 = Box::pin(
248 futures::stream::once(async move { head.clone() })
249 .chain(futures::stream::once(async move { first_app_chunk }).chain(stm2))
250 .map(|html| html.into_bytes()),
251 );
252
253 let first_chunk = stm3.next().await;
254 let second_chunk = stm3.next().await;
255
256 let status_code = resp_opts.status().unwrap_or(200);
257 let headers = resp_opts.headers();
258
259 let og = OutgoingResponse::new(headers);
260 og.set_status_code(status_code).expect("Failed to set status");
261 let mut ogbod = og.take_body();
262 resp_out.set(og);
263
264 let mut stm4 = Box::pin(
265 futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap()])
266 .chain(stm3)
267 .chain(
268 futures::stream::once(async move {
269 runtime.dispose();
270 tail.to_string()
271 })
272 .map(|html| html.into_bytes()),
273 ),
274 );
275
276 while let Some(ch) = stm4.next().await {
277 ogbod.send(ch).await.unwrap();
278 }
279}
280
281pub fn redirect(path: &str) {
295 if let (Some(req), Some(res)) = (
296 use_context::<RequestParts>(),
297 use_context::<ResponseOptions>(),
298 ) {
299 res.insert_header("location", path);
301
302 let req_headers = Headers::from_list(req.headers()).expect("Failed to construct Headers from Request Headers");
303 let accepts_html = &req_headers.get(&"Accept".to_string())[0];
304 let accepts_html_bool = String::from_utf8_lossy(accepts_html).contains("text/html");
305 if accepts_html_bool {
306 res.set_status(302);
309 } else {
310 res.insert_header(REDIRECT_HEADER, "");
314 }
315 } else {
316 tracing::warn!(
317 "Couldn't retrieve either Parts or ResponseOptions while trying \
318 to redirect()."
319 );
320 }
321}
322
323fn provide_contexts(url: &url::Url, req_parts: RequestParts, res_options: ResponseOptions, additional_context: impl Fn() + Clone + Send + 'static) {
324 let path = leptos_corrected_path(url);
325
326 let integration = leptos_router::ServerIntegration { path };
327 provide_context(leptos_router::RouterIntegrationContext::new(integration));
328 provide_context(leptos_meta::MetaContext::new());
329 provide_context(res_options);
330 provide_context(req_parts);
331 additional_context();
332 leptos_router::provide_server_redirect(redirect);
333 #[cfg(feature = "nonce")]
334 leptos::nonce::provide_nonce();
335}
336
337fn leptos_corrected_path(req: &url::Url) -> String {
338 let path = req.path();
339 let query = req.query();
340 if query.unwrap_or_default().is_empty() {
341 "http://leptos".to_string() + path
342 } else {
343 "http://leptos".to_string() + path + "?" + query.unwrap_or_default()
344 }
345}
346
347fn url(req: &IncomingRequest) -> String {
348 let full_url = &req.headers().get(&"spin-full-url".to_string())[0];
349 String::from_utf8_lossy(full_url).to_string()
350}
351
352fn log_and_server_error(message: impl Into<String>, resp_out: ResponseOutparam) {
353 println!("Error: {}", message.into());
354 let response = spin_sdk::http::OutgoingResponse::new(spin_sdk::http::Fields::new());
355 response.set_status_code(500).expect("Failed to set status");
356 resp_out.set(response);
357}