mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
feat: provide static file handling/fallback directly in integration
This commit is contained in:
parent
2a558aa3f0
commit
2916873985
3 changed files with 127 additions and 41 deletions
|
@ -26,6 +26,8 @@ leptos_integration_utils = { workspace = true }
|
|||
parking_lot = "0.12"
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", default-features = false }
|
||||
tower = "0.4"
|
||||
tower-http = "0.5"
|
||||
tracing = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -34,11 +34,11 @@
|
|||
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
extract::{FromRef, FromRequestParts, MatchedPath},
|
||||
extract::{FromRef, FromRequestParts, MatchedPath, State},
|
||||
http::{
|
||||
header::{self, HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER},
|
||||
request::Parts,
|
||||
HeaderMap, Method, Request, Response, StatusCode,
|
||||
HeaderMap, Method, Request, Response, StatusCode, Uri,
|
||||
},
|
||||
response::IntoResponse,
|
||||
routing::{delete, get, patch, post, put},
|
||||
|
@ -47,6 +47,7 @@ use futures::{stream::once, Future, Stream, StreamExt};
|
|||
use leptos::{
|
||||
config::LeptosOptions,
|
||||
context::{provide_context, use_context},
|
||||
prelude::*,
|
||||
reactive_graph::{computed::ScopedFuture, owner::Owner},
|
||||
IntoView,
|
||||
};
|
||||
|
@ -61,6 +62,8 @@ use leptos_router::{
|
|||
use parking_lot::RwLock;
|
||||
use server_fn::{redirect::REDIRECT_HEADER, ServerFnError};
|
||||
use std::{fmt::Debug, io, pin::Pin, sync::Arc};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
// use tracing::Instrument; // TODO check tracing span -- was this used in 0.6 for a missing link?
|
||||
|
||||
/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
|
||||
|
@ -740,46 +743,60 @@ where
|
|||
move |req: Request<Body>| {
|
||||
let app_fn = app_fn.clone();
|
||||
let additional_context = additional_context.clone();
|
||||
Box::pin(async move {
|
||||
let app_fn = app_fn.clone();
|
||||
let add_context = additional_context.clone();
|
||||
let res_options = ResponseOptions::default();
|
||||
let meta_context = ServerMetaContext::new();
|
||||
|
||||
let additional_context = {
|
||||
let meta_context = meta_context.clone();
|
||||
let res_options = res_options.clone();
|
||||
move || {
|
||||
// Need to get the path and query string of the Request
|
||||
// For reasons that escape me, if the incoming URI protocol is https, it provides the absolute URI
|
||||
let path = req.uri().path_and_query().unwrap().as_str();
|
||||
|
||||
let full_path = format!("http://leptos.dev{path}");
|
||||
let (_, req_parts) = generate_request_and_parts(req);
|
||||
provide_contexts(
|
||||
&full_path,
|
||||
&meta_context,
|
||||
req_parts,
|
||||
res_options.clone(),
|
||||
);
|
||||
add_context();
|
||||
}
|
||||
};
|
||||
|
||||
let res = AxumResponse::from_app(
|
||||
app_fn,
|
||||
meta_context,
|
||||
additional_context,
|
||||
res_options,
|
||||
stream_builder,
|
||||
)
|
||||
.await;
|
||||
|
||||
res.0
|
||||
})
|
||||
handle_response_inner(additional_context, app_fn, req, stream_builder)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_response_inner<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl FnOnce() -> IV + Send + 'static,
|
||||
req: Request<Body>,
|
||||
stream_builder: fn(
|
||||
IV,
|
||||
BoxedFnOnce<PinnedStream<String>>,
|
||||
) -> PinnedFuture<PinnedStream<String>>,
|
||||
) -> PinnedFuture<Response<Body>>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let add_context = additional_context.clone();
|
||||
let res_options = ResponseOptions::default();
|
||||
let meta_context = ServerMetaContext::new();
|
||||
|
||||
let additional_context = {
|
||||
let meta_context = meta_context.clone();
|
||||
let res_options = res_options.clone();
|
||||
move || {
|
||||
// Need to get the path and query string of the Request
|
||||
// For reasons that escape me, if the incoming URI protocol is https, it provides the absolute URI
|
||||
let path = req.uri().path_and_query().unwrap().as_str();
|
||||
|
||||
let full_path = format!("http://leptos.dev{path}");
|
||||
let (_, req_parts) = generate_request_and_parts(req);
|
||||
provide_contexts(
|
||||
&full_path,
|
||||
&meta_context,
|
||||
req_parts,
|
||||
res_options.clone(),
|
||||
);
|
||||
add_context();
|
||||
}
|
||||
};
|
||||
|
||||
let res = AxumResponse::from_app(
|
||||
app_fn,
|
||||
meta_context,
|
||||
additional_context,
|
||||
res_options,
|
||||
stream_builder,
|
||||
)
|
||||
.await;
|
||||
|
||||
res.0
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
fn provide_contexts(
|
||||
path: &str,
|
||||
|
@ -1698,3 +1715,70 @@ where
|
|||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(format!("{e:?}")))
|
||||
}
|
||||
|
||||
pub fn file_and_error_handler<S, IV>(
|
||||
shell: fn(LeptosOptions) -> IV,
|
||||
) -> impl Fn(
|
||||
Uri,
|
||||
State<S>,
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ 'static
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
S: Send + 'static,
|
||||
LeptosOptions: FromRef<S>,
|
||||
{
|
||||
move |uri: Uri, State(options): State<S>, req: Request<Body>| {
|
||||
Box::pin(async move {
|
||||
let options = LeptosOptions::from_ref(&options);
|
||||
let res = get_static_file(uri, &options.site_root);
|
||||
let res = res.await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let mut res = handle_response_inner(
|
||||
|| {},
|
||||
move || shell(options),
|
||||
req,
|
||||
|app, chunks| {
|
||||
Box::pin(async move {
|
||||
let app = app
|
||||
.to_html_stream_in_order()
|
||||
.collect::<String>()
|
||||
.await;
|
||||
let chunks = chunks();
|
||||
Box::pin(once(async move { app }).chain(chunks))
|
||||
as PinnedStream<String>
|
||||
})
|
||||
},
|
||||
)
|
||||
.await;
|
||||
*res.status_mut() = StatusCode::NOT_FOUND;
|
||||
res
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
root: &str,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(&*root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {err}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ pub trait ExtendResponse: Sized {
|
|||
fn set_default_content_type(&mut self, content_type: &str);
|
||||
|
||||
fn from_app<IV>(
|
||||
app_fn: impl Fn() -> IV + Send + 'static,
|
||||
app_fn: impl FnOnce() -> IV + Send + 'static,
|
||||
meta_context: ServerMetaContext,
|
||||
additional_context: impl FnOnce() + Send + 'static,
|
||||
res_options: Self::ResponseOptions,
|
||||
|
@ -70,7 +70,7 @@ pub trait ExtendResponse: Sized {
|
|||
}
|
||||
|
||||
pub fn build_response<IV>(
|
||||
app_fn: impl Fn() -> IV + Send + 'static,
|
||||
app_fn: impl FnOnce() -> IV + Send + 'static,
|
||||
meta_context: ServerMetaContext,
|
||||
additional_context: impl FnOnce() + Send + 'static,
|
||||
stream_builder: fn(
|
||||
|
|
Loading…
Reference in a new issue