leptos-rs / leptos

Build fast web applications with Rust.
https://leptos.dev
MIT License
15.97k stars 627 forks source link

Error when running server function inside spawn_local #2562

Closed luxalpa closed 5 months ago

luxalpa commented 5 months ago

Describe the bug When using spawn_local to get data from the server instead of fetching from inside a resource, then the extract function inside the server function will cause an error: "HttpRequest should have been provided via context".

Leptos Dependencies

actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_error_panic_hook = "0.1"
cfg-if = "1"
http = { version = "0.2", optional = true }
leptos = { version = "0.6.11", features = ["nightly"] }
leptos_meta = { version = "0.6.11", features = ["nightly"] }
leptos_actix = { version = "0.6.11", optional = true }
leptos_router = { version = "0.6.11", features = ["nightly"] }
leptos_dom = "0.6.11"
leptos_reactive = { version = "0.6.11" }
leptos_macro = { version = "0.6.11" }
serde = "1.0.198"

To Reproduce

use leptos::*;
use leptos_meta::*;
use leptos_router::{Route, Router, Routes, SsrMode};
use std::time::Duration;

#[component]
pub fn App() -> impl IntoView {
    provide_meta_context();

    view! {
        <Router>
            <div>
                "Hello World"
                    <Routes>
                        <Route path="/" view=|| {
                            view! {<FrontPage />}
                        } ssr=SsrMode::PartiallyBlocked />
                    </Routes>
            </div>
        </Router>
    }
}

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct UserData {
    name: String,
}

#[server]
pub async fn get_user_data() -> Result<UserData, ServerFnError> {
    use actix_web::HttpRequest;
    use leptos_actix::extract;

    let req: HttpRequest = extract().await.unwrap();
    println!("Method: {:?}", req.method());

    return Ok(UserData {
        name: "Luxalpa".to_string(),
    });
}

#[component]
pub fn FrontPage() -> impl IntoView {
    spawn_local(async move {
        let site_data = get_user_data().await.unwrap();
        println!("Site Data: {:?}", site_data);
    });

    view! { <div> "Front Page" </div> }
}

Additional Context Using reqwest instead of a server function seems to be a workaround.

gbj commented 5 months ago

Not sure what the goal is during the example -- I see the panic and it is during initial route generation on the server. This kind of error would be suppressed during route generation using something like a resource. Is the idea here to run some async task, independent of the rendering process, but while rendering? There's no way to wait for the task you spawned to finish before rendering HTML.

gbj commented 5 months ago

Or of course you can just not unwrap in the places you're unwrapping; this version works fine:

#[server]
pub async fn get_user_data() -> Result<UserData, ServerFnError> {
    use actix_web::HttpRequest;
    use leptos_actix::extract;

    let req: HttpRequest = extract().await?;
    println!("Method: {:?}", req.method());

    return Ok(UserData {
        name: "Luxalpa".to_string(),
    });
}

#[component]
pub fn FrontPage() -> impl IntoView {
    spawn_local(async move {
        if let Ok(site_data) = get_user_data().await {
        println!("Site Data: {:?}", site_data);

        }
    });

    view! { <div> "Front Page" </div> }
}
luxalpa commented 5 months ago

The code above is just a minimalistic example. My application is actually using a resource that is set as pending like this:

let resource = create_blocking_resource(
    move || {},
    // TODO: Figure out if this future needs to be manually cleaned up somehow
    move |_| future::pending::<CardWithPrinting>(),
);

and then later (in the same function) it is fetching the data:

spawn_local(async move {
    let result = get_card_printings(ids).await.unwrap().0;
    self.add_to_cache(result);
});

and in add_to_cache it is setting the resource:

// Notify resources
if let Some(resources) = in_use.remove(&card.printing.id) {
    resources.iter().for_each(|r| {
        r.try_set(card.clone());
    });
}

Because the application needs to cache the data that comes from these resources while also providing SSR for them too. This architecture was inspired by leptos_query, but apparently it does not work well with server functions.

gbj commented 5 months ago

To be clear the panic is unrelated to server functions, it specifically has to do with unwrapping the attempt to extract an HttpRequest during the server's initial route generation, which runs the application once to figure out all the routes you've defined. It suppresses resource loads during this, but the spawn_local still runs, and panics when unwrapping the HttpRequest extractor. (There is no HTTP request during that route generation process.)

In the actual example you provide, I assume that just not unwrapping here will fix your issue:

spawn_local(async move {
    let result = get_card_printings(ids).await.unwrap().0;
    self.add_to_cache(result);
});

// instead
spawn_local(async move {
    // I'm missing the .0 here but hopefully you see what I mean
    if let Ok(result) = get_card_printings(ids).await {
      self.add_to_cache(result);
    }
});

and likewise removing the .unwrap() in get_card_printings and using ? instead if the extractor fails.

luxalpa commented 5 months ago

ok, thanks a lot, that actually explains some things and allowed me to workaround this.