Skip to content

Commit

Permalink
feat: provide two different Url constructors: with and without base
Browse files Browse the repository at this point in the history
The URL constructor without base (`Url::try_from`) requires an
absolute URL, so we need to enforce this for all callers. The only
caller which called this with a relative path was `create_location()`,
so I changed `create_location` to always prepend `http://leptos.dev`
to the path. The origin in this case doesn't matter, because
`create_location()` doesn't use the origin.

However, there was an inconsistency between client and server:
previosly, `create_location()` was called with a relative path on
the client and an absolute URL (with origin `http://leptos.rs` or
`http://leptos.dev`) on the server. I fixed this by always using a
relative path on the server, too.
  • Loading branch information
haslersn committed Feb 8, 2024
1 parent f1bc734 commit 5559c77
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 84 deletions.
26 changes: 8 additions & 18 deletions integrations/axum/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -659,12 +659,11 @@ where
// 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
// if http, it returns a relative path. Adding .path() seems to make it explicitly return the relative uri
let path = req.uri().path_and_query().unwrap().as_str();
let path = req.uri().path_and_query().unwrap().to_string();

let full_path = format!("http://leptos.dev{path}");
let (_, req_parts) = generate_request_and_parts(req);
move || {
provide_contexts(full_path, req_parts, default_res_options);
provide_contexts(path, req_parts, default_res_options);
app_fn().into_view()
}
};
Expand Down Expand Up @@ -816,18 +815,15 @@ where
// 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
// if http, it returns a relative path. Adding .path() seems to make it explicitly return the relative uri
let path = req.uri().path_and_query().unwrap().as_str();

let full_path = format!("http://leptos.dev{path}");
let path = req.uri().path_and_query().unwrap().to_string();

let (tx, rx) = futures::channel::mpsc::channel(8);
let current_span = tracing::Span::current();
spawn_task!(async move {
let app = {
let full_path = full_path.clone();
let (parts, _) = req.into_parts();
move || {
provide_contexts(full_path, parts, default_res_options);
provide_contexts(path, parts, default_res_options);
app_fn().into_view()
}
};
Expand Down Expand Up @@ -988,18 +984,15 @@ where
// 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
// if http, it returns a relative path. Adding .path() seems to make it explicitly return the relative uri
let path = req.uri().path_and_query().unwrap().as_str();

let full_path = format!("http://leptos.dev{path}");
let path = req.uri().path_and_query().unwrap().to_string();

let (tx, rx) = futures::channel::oneshot::channel();
spawn_task!(async move {
let app = {
let full_path = full_path.clone();
let (_, req_parts) = generate_request_and_parts(req);
move || {
provide_contexts(
full_path,
path,
req_parts,
default_res_options,
);
Expand Down Expand Up @@ -1117,19 +1110,16 @@ where
// 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
// if http, it returns a relative path. Adding .path() seems to make it explicitly return the relative uri
let path = req.uri().path_and_query().unwrap().as_str();

let full_path = format!("http://leptos.dev{path}");
let path = req.uri().path_and_query().unwrap().to_string();

let (tx, rx) = futures::channel::oneshot::channel();

spawn_task!(async move {
let app = {
let full_path = full_path.clone();
let (_, req_parts) = generate_request_and_parts(req);
move || {
provide_contexts(
full_path,
path,
req_parts,
default_res_options,
);
Expand Down
2 changes: 1 addition & 1 deletion router/src/extract_routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ where
let runtime = create_runtime();

let integration = ServerIntegration {
path: "http://leptos.rs/".to_string(),
path: "/".to_string(),
};

provide_context(RouterIntegrationContext::new(integration));
Expand Down
19 changes: 14 additions & 5 deletions router/src/history/location.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,22 @@ pub fn create_location(
state: ReadSignal<State>,
) -> Location {
let url = create_memo(move |prev: Option<&Url>| {
path.with(|path| match Url::try_from(path.as_str()) {
Ok(url) => url,
Err(e) => {
path.with(|path| {
if !path.starts_with("/") {
leptos::logging::error!(
"[Leptos Router] Invalid path {path}\n\n{e:?}"
"[Leptos Router] Invalid path {path}\n\nmust start with a slash"
);
prev.cloned().unwrap()
return prev.cloned().unwrap();
}
let full_path = format!("http://leptos.dev{path}");
match Url::try_from(full_path.as_str()) {
Ok(url) => url,
Err(e) => {
leptos::logging::error!(
"[Leptos Router] Invalid path {path}\n\n{e:?}"
);
prev.cloned().unwrap()
}
}
})
});
Expand Down
5 changes: 2 additions & 3 deletions router/src/history/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ pub(crate) fn scroll_to_el(loc_scroll: bool) {
/// # use leptos::*;
/// # let rt = create_runtime();
/// let integration = ServerIntegration {
/// path: "http://leptos.rs/".to_string(),
/// path: "/".to_string(),
/// };
/// provide_context(RouterIntegrationContext::new(integration));
/// # rt.dispose();
Expand Down Expand Up @@ -195,8 +195,7 @@ impl History for RouterIntegrationContext {
/// # use leptos::*;
/// # let rt = create_runtime();
/// let integration = ServerIntegration {
/// // Swap out with your URL if integrating manually.
/// path: "http://leptos.rs/".to_string(),
/// path: "/".to_string(),
/// };
/// provide_context(RouterIntegrationContext::new(integration));
/// # rt.dispose();
Expand Down
137 changes: 80 additions & 57 deletions router/src/history/url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,69 +31,72 @@ pub fn escape(s: &str) -> String {
js_sys::encode_uri(s).as_string().unwrap()
}

#[cfg(not(feature = "ssr"))]
impl TryFrom<&str> for Url {
type Error = String;
impl Url {
#[cfg(not(feature = "ssr"))]
pub fn new_with_base(url: &str, base: &str) -> Result<Self, String> {
let url = web_sys::Url::new_with_base(url, base).map_js_error()?;
Self::from_web_sys_url(&url)
}

fn try_from(url: &str) -> Result<Self, Self::Error> {
let url = web_sys::Url::new_with_base(
&if url.starts_with("//") {
let origin =
leptos::window().location().origin().unwrap_or_default();
format!("{origin}{url}")
} else {
url.to_string()
#[cfg(not(feature = "ssr"))]
fn from_web_sys_url(url: &web_sys::Url) -> Result<Self, String> {
Ok(
Self {
origin: url.origin(),
pathname: url.pathname(),
search: url
.search()
.strip_prefix('?')
.map(String::from)
.unwrap_or_default(),
search_params:
ParamsMap(
try_iter(&url.search_params())
.map_js_error()?
.ok_or(
"Failed to use URLSearchParams as an iterator"
.to_string(),
)?
.map(|value| {
let array: Array = value
.map_js_error()?
.dyn_into()
.map_js_error()?;
Ok((
array
.get(0)
.dyn_into::<JsString>()
.map_js_error()?
.into(),
array
.get(1)
.dyn_into::<JsString>()
.map_js_error()?
.into(),
))
})
.collect::<Result<
linear_map::LinearMap<String, String>,
String,
>>()?,
),
hash: url.hash(),
},
"http://leptos",
)
.map_js_error()?;
Ok(Self {
origin: url.origin(),
pathname: url.pathname(),
search: url
.search()
.strip_prefix('?')
.map(String::from)
.unwrap_or_default(),
search_params: ParamsMap(
try_iter(&url.search_params())
.map_js_error()?
.ok_or(
"Failed to use URLSearchParams as an iterator"
.to_string(),
)?
.map(|value| {
let array: Array =
value.map_js_error()?.dyn_into().map_js_error()?;
Ok((
array
.get(0)
.dyn_into::<JsString>()
.map_js_error()?
.into(),
array
.get(1)
.dyn_into::<JsString>()
.map_js_error()?
.into(),
))
})
.collect::<Result<
linear_map::LinearMap<String, String>,
Self::Error,
>>()?,
),
hash: url.hash(),
})
}
}

#[cfg(feature = "ssr")]
impl TryFrom<&str> for Url {
type Error = String;
#[cfg(feature = "ssr")]
pub fn new_with_base(url: &str, base: &str) -> Result<Self, String> {
let base = url::Url::parse(base).map_err(|e| e.to_string())?;
let url = url::Url::options()
.base_url(Some(&base))
.parse(url)
.map_err(|e| e.to_string())?;
Self::from_servo_url(&url)
}

fn try_from(url: &str) -> Result<Self, Self::Error> {
let url = url::Url::parse(url).map_err(|e| e.to_string())?;
#[cfg(feature = "ssr")]
fn from_servo_url(url: &url::Url) -> Result<Self, String> {
Ok(Self {
origin: url.origin().unicode_serialization(),
pathname: url.path().to_string(),
Expand All @@ -108,6 +111,26 @@ impl TryFrom<&str> for Url {
}
}

#[cfg(not(feature = "ssr"))]
impl TryFrom<&str> for Url {
type Error = String;

fn try_from(url: &str) -> Result<Self, Self::Error> {
let url = web_sys::Url::new(url).map_js_error()?;
Self::from_web_sys_url(&url)
}
}

#[cfg(feature = "ssr")]
impl TryFrom<&str> for Url {
type Error = String;

fn try_from(url: &str) -> Result<Self, Self::Error> {
let url = url::Url::parse(url).map_err(|e| e.to_string())?;
Self::from_servo_url(&url)
}
}

#[cfg(not(feature = "ssr"))]
trait MapJsError<T> {
fn map_js_error(self) -> Result<T, String>;
Expand Down

0 comments on commit 5559c77

Please sign in to comment.