Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSR + hydration results in a stalled request #3619

Open
1 of 3 tasks
christobill opened this issue Mar 1, 2024 · 0 comments
Open
1 of 3 tasks

SSR + hydration results in a stalled request #3619

christobill opened this issue Mar 1, 2024 · 0 comments
Labels

Comments

@christobill
Copy link

christobill commented Mar 1, 2024

Problem
When building a small SSR + hydration web app and opening in the browser
we get a part of the html but we have to wait the end of the server-side request to have the rest of the html.
The request is stalled, there is no hydration nor fallback UI (they are configured, though)

Steps To Reproduce
Starting from the example ssr_router, filling src/lib.rs and using App and ServerApp in the other files:

use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
use yew::html::RenderError;
use yew::prelude::*;
use yew_router::history::{AnyHistory, History, MemoryHistory};
use yew_router::prelude::*;

#[derive(Debug, Clone, Copy, PartialEq, Routable)]
enum Route {
    #[at("/")]
    Home,
    #[at("/hi")]
    Hi,
}

#[cfg(target_arch = "wasm32")]
async fn async_delay<F>(f: F, duration: u64)
where
    F: FnOnce() + 'static,
{
    use gloo_timers::future::TimeoutFuture;
    TimeoutFuture::new(duration as u32).await;
    f()
}

#[cfg(not(target_arch = "wasm32"))]
async fn async_delay<F>(f: F, duration: u64)
where
    F: FnOnce() + 'static,
{
    use tokio::time::{sleep, Duration};
    sleep(Duration::from_millis(duration)).await;
    f()
}

#[derive(Serialize, Deserialize)]
struct UuidResponse {
    uuid: Uuid,
}

#[cfg(feature = "ssr")]
async fn fetch_uuid() -> Uuid {
    // reqwest works for both non-wasm and wasm targets.
    let resp = reqwest::get("https://httpbin.org/uuid").await.unwrap();
    async_delay(|| (), 30000).await;
    let uuid_resp = resp.json::<UuidResponse>().await.unwrap();

    uuid_resp.uuid
}

#[function_component]
fn Nav() -> Html {
    html! {
        <ul>
            <li><Link<Route> to={Route::Home}>{"Home"}</Link<Route>></li>
            <li><Link<Route> to={Route::Hi}>{"Hi"}</Link<Route>></li>
        </ul>
    }
}

#[function_component]
fn Content() -> HtmlResult {
    let uuid =
        use_prepared_state!((), async move |_| -> Uuid { fetch_uuid().await }).map_err(|e| {
            println!("error: {:#?}", e);
            e
        });

    let uuid2 = (match uuid {
        Ok(Some(x)) => Ok(html! {
            <div>{"Random UUID: "}{x}</div>
        }),
        Ok(None) => Ok(html! {
            <div>{"Loading"}</div>
        }),
        Err(e) => Err(RenderError::Suspended(e)),
    });

    uuid2
}

#[function_component]
fn Hi() -> HtmlResult {
    Ok(html! {
        <div>{"Hi"}</div>
    })
}

#[derive(Properties, PartialEq, Eq, Debug)]
pub struct ServerAppProps {
    pub url: AttrValue,
    pub queries: HashMap<String, String>,
}

#[function_component]
pub fn ServerApp(props: &ServerAppProps) -> Html {
    let history = AnyHistory::from(MemoryHistory::new());
    history
        .push_with_query(&*props.url, &props.queries)
        .unwrap();

    html! {
        <Router history={history}>
            <Nav />
            <Switch<Route> render={switch} />
        </Router>
    }
}

#[function_component]
pub fn App() -> Html {
    html! {
        <BrowserRouter>
            <Nav />
            <Switch<Route> render={switch} />
        </BrowserRouter>
    }
}

fn switch(routes: Route) -> Html {
    match routes {
        Route::Hi => html! {<Hi />},
        Route::Home => {
            let fallback = html! {<div>{"Loading..."}</div>};
            html! {
                <Suspense {fallback}><Content /></Suspense>
            }
        }
    }
}

and src/bin/ssr_router_server

use std::collections::HashMap;
use std::convert::Infallible;
use std::path::PathBuf;

use axum::body::{Body, StreamBody};
use axum::error_handling::HandleError;
use axum::extract::Query;
use axum::handler::Handler;
use axum::http::{Request, StatusCode};
use axum::response::IntoResponse;
use axum::routing::get;
use axum::{Extension, Router};
use clap::Parser;
use futures::stream::{self, StreamExt};
use hyper::server::Server;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use yew::ServerRenderer;

use simple_ssr::{ServerApp, ServerAppProps};

/// A basic example
#[derive(Parser, Debug)]
struct Opt {
    /// the "dist" created by trunk directory to be served for hydration.
    #[structopt(short, long, parse(from_os_str))]
    dir: PathBuf,
}

async fn render(
    Extension((index_html_before, index_html_after)): Extension<(String, String)>,
    url: Request<Body>,
    Query(queries): Query<HashMap<String, String>>,
) -> impl IntoResponse {
    let url = url.uri().to_string();

    let renderer = ServerRenderer::<ServerApp>::with_props(move || ServerAppProps {
        url: url.into(),
        queries,
    });

    StreamBody::new(
        stream::once(async move { index_html_before })
            .chain(renderer.render_stream())
            .chain(stream::once(async move { index_html_after }))
            .map(Result::<_, Infallible>::Ok),
    )
}

#[tokio::main]
async fn main() {
    let opts = Opt::parse();

    let index_html_s = tokio::fs::read_to_string(opts.dir.join("index.html"))
        .await
        .expect("failed to read index.html");

    let (index_html_before, index_html_after) = index_html_s.split_once("<body>").unwrap();
    let mut index_html_before = index_html_before.to_owned();
    index_html_before.push_str("<body>");
    let index_html_after = index_html_after.to_owned();

    let handle_error = |e| async move {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            format!("error occurred: {}", e),
        )
    };

    let app = Router::new()
        .route("/api/test", get(|| async move { "Hello World" }))
        .fallback(HandleError::new(
            ServeDir::new(opts.dir)
                .append_index_html_on_directories(false)
                .fallback(
                    render
                        .layer(Extension((
                            index_html_before.clone(),
                            index_html_after.clone(),
                        )))
                        .into_service()
                        .map_err(|err| -> std::io::Error { match err {} }),
                ),
            handle_error,
        ));

    println!("You can view the website at: http://localhost:8081/");
    Server::bind(&"127.0.0.1:8081".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

Launch the server:
trunk build --release index.html && cargo run --release --features=ssr --bin ssr_router_server -- --dir=dist

Go to the url http://localhost:8081 and wait 30 seconds for the request to https://httpbin.org/uuid to complete:
You will see part of the UI, but no fallback UI. If you curl "http://localhost:8081", you will have to wait 30 seconds to get an answer

Expected behavior
Server side rendering should display <div>Loading...</div>
and hydration should hydrate with the request result: <div>Random UUID: 1be9a2ec-c62e-40af-9b38-9fccae823f84</div>

Environment:

  • Yew version: [v0.21]

Questionnaire

  • I'm interested in fixing this myself but don't know where to start
  • I would like to fix and I have a solution
  • I don't have time to fix this right now, but maybe later
@christobill christobill added the bug label Mar 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant