fix: serve chooser before workspace creation

This commit is contained in:
2026-08-21 04:51:44 +09:00
parent 5dd9392575
commit e8c159247a
+89 -10
View File
@@ -897,6 +897,9 @@ async fn dispatch_workspace_request(
Ok(workspaces) => workspaces, Ok(workspaces) => workspaces,
Err(error) => return server_error_response(error), Err(error) => return server_error_response(error),
}; };
if workspaces.is_empty() && is_server_static_forward(path) {
return serve_server_static_shell(&api, path).await;
}
if workspaces.len() == 1 || is_server_global_forward(path) { if workspaces.len() == 1 || is_server_global_forward(path) {
match workspaces.first() { match workspaces.first() {
Some(workspace) => match api.router_for_workspace(&workspace.workspace_id).await { Some(workspace) => match api.router_for_workspace(&workspace.workspace_id).await {
@@ -927,6 +930,17 @@ fn is_server_global_forward(path: &str) -> bool {
|| path.starts_with("/assets/") || path.starts_with("/assets/")
} }
fn is_server_static_forward(path: &str) -> bool {
path == "/" || path.starts_with("/_app/") || path.starts_with("/assets/")
}
async fn serve_server_static_shell(api: &WorkspaceServerApi, path: &str) -> Response {
let Some(static_root) = api.template.static_assets_dir.as_ref() else {
return StatusCode::NOT_FOUND.into_response();
};
static_file_or_spa_response(static_root, path).await
}
fn scoped_workspace_id(path: &str) -> Option<&str> { fn scoped_workspace_id(path: &str) -> Option<&str> {
let mut segments = path.trim_start_matches('/').split('/'); let mut segments = path.trim_start_matches('/').split('/');
match (segments.next(), segments.next(), segments.next()) { match (segments.next(), segments.next(), segments.next()) {
@@ -12503,16 +12517,7 @@ async fn static_or_spa_fallback(State(api): State<WorkspaceApi>, uri: Uri) -> Re
return StatusCode::NOT_FOUND.into_response(); return StatusCode::NOT_FOUND.into_response();
}; };
match read_static_or_index(static_root, scoped_workspace_static_path(uri.path())).await { static_file_or_spa_response(static_root, scoped_workspace_static_path(uri.path())).await
Ok(StaticAsset {
bytes,
content_type,
}) => (StatusCode::OK, [(CONTENT_TYPE, content_type)], bytes).into_response(),
Err(error) => {
tracing::debug!(%error, path = %uri.path(), "failed to serve static asset");
StatusCode::NOT_FOUND.into_response()
}
}
} }
fn unscoped_workspace_ui_redirect( fn unscoped_workspace_ui_redirect(
@@ -12552,6 +12557,19 @@ struct StaticAsset {
content_type: &'static str, content_type: &'static str,
} }
async fn static_file_or_spa_response(static_root: &Path, request_path: &str) -> Response {
match read_static_or_index(static_root, request_path).await {
Ok(StaticAsset {
bytes,
content_type,
}) => (StatusCode::OK, [(CONTENT_TYPE, content_type)], bytes).into_response(),
Err(error) => {
tracing::debug!(%error, path = request_path, "failed to serve static asset");
StatusCode::NOT_FOUND.into_response()
}
}
}
fn scoped_workspace_static_path(path: &str) -> &str { fn scoped_workspace_static_path(path: &str) -> &str {
let Some(scoped) = path.strip_prefix("/w/") else { let Some(scoped) = path.strip_prefix("/w/") else {
return path; return path;
@@ -14429,6 +14447,67 @@ mod tests {
config config
} }
#[tokio::test]
async fn server_router_serves_workspace_chooser_before_first_workspace_exists() {
let dir = tempfile::tempdir().unwrap();
let static_dir = dir.path().join("static");
std::fs::create_dir_all(static_dir.join("_app/immutable/entry")).unwrap();
std::fs::write(
static_dir.join("index.html"),
"<main>Workspace chooser</main>",
)
.unwrap();
std::fs::write(
static_dir.join("_app/immutable/entry/start.js"),
"console.log('workspace app');",
)
.unwrap();
let mut template = test_server_config(dir.path());
template.static_assets_dir = Some(static_dir);
let store = Arc::new(SqliteWorkspaceStore::open(&template.database_path).unwrap());
let app = build_workspace_server_router(template, store)
.await
.unwrap();
for (uri, expected) in [
("/", "<main>Workspace chooser</main>"),
(
"/_app/immutable/entry/start.js",
"console.log('workspace app');",
),
] {
let response = app
.clone()
.oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
String::from_utf8(
to_bytes(response.into_body(), usize::MAX)
.await
.unwrap()
.to_vec(),
)
.unwrap(),
expected
);
}
let response = app
.oneshot(
Request::builder()
.uri("/api/workspaces")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
assert_eq!(serde_json::from_slice::<Value>(&body).unwrap(), json!([]));
}
#[tokio::test] #[tokio::test]
async fn server_router_dispatches_two_workspace_contexts_without_state_leakage() { async fn server_router_dispatches_two_workspace_contexts_without_state_leakage() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();