Skip to content

feat(server): 添加静态资源嵌入支持并优化中间件配置#2

Merged
sj817 merged 1 commit into
mainfrom
res
May 8, 2026
Merged

feat(server): 添加静态资源嵌入支持并优化中间件配置#2
sj817 merged 1 commit into
mainfrom
res

Conversation

@wuliya336
Copy link
Copy Markdown
Contributor

@wuliya336 wuliya336 commented May 8, 2026

  • 引入 rust-embed 依赖以支持编译时静态资源嵌入
  • 移除自定义 AssetCache 中间件,改用 rust-embed 实现静态文件服务
  • 配置开发环境使用 ServeDir,生产环境使用嵌入式静态资源
  • 简化 HTTP 客户端初始化和服务构建逻辑
  • 优化路由配置和错误处理流程

Summary by CodeRabbit

  • Chores

    • Updated project dependencies and workspace configuration for improved asset management.
  • Refactor

    • Restructured static asset serving mechanism to use embedded assets in production builds while maintaining file-based serving in development mode.

- 引入 rust-embed 依赖以支持编译时静态资源嵌入
- 移除自定义 AssetCache 中间件,改用 rust-embed 实现静态文件服务
- 配置开发环境使用 ServeDir,生产环境使用嵌入式静态资源
- 简化 HTTP 客户端初始化和服务构建逻辑
- 优化路由配置和错误处理流程
@wuliya336 wuliya336 requested a review from sj817 May 8, 2026 13:36
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR integrates the rust-embed crate to serve static web assets from compiled binaries. Development mode continues using the filesystem, while production uses embedded assets. The legacy AssetCache caching wrapper is removed. Minor improvements include simplified lifetime annotations and improved Rust idioms in the sync module.

Changes

Embedded Asset Serving Integration

Layer / File(s) Summary
Dependencies and Build Configuration
Cargo.toml, pnpm-workspace.yaml
rust-embed = { version = "8.11.0", features = ["axum"] } is added. Workspace config adds onlyBuiltDependencies for @parcel/watcher.
Server Imports and Type Cleanup
src/server.rs
Imports updated to use http::StatusCode. The AssetCache wrapper type and its associated tower service implementation are removed.
Request Handler and Logging Updates
src/server.rs
reject_query formatting is refactored. TraceLayer::on_response closure updated to log with warn for 5xx responses and info otherwise. Auth header extraction condensed to a single-line assignment.
Static Asset Handler Implementation
src/server.rs
New #[cfg(not(debug_assertions))] static_handler function serves embedded assets from webui/dist, derives Content-Type using mime_guess, maps request paths to embed keys, and returns 404 for missing assets.
Server Fallback and Listener Configuration
src/server.rs
Router fallback logic restructured: debug mode uses fallback_service(ServeDir::new("webui")), non-debug uses fallback(static_handler). TCP listener binding and display_host computation updated.
Formatting and Idiom Updates
src/server.rs, src/sync.rs
Minor formatting changes in Client builder and handle_raw call. Lifetime annotation in WhitelistKind::url elided. Duration::from_secs(interval_secs) called directly without u64 cast.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Embedded assets hop in neat,
No more serving from the street,
Cache-Control wrappers fade away,
Static files compiled to stay,
Debug mode springs back to the source—
Of course! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title in Chinese accurately describes the main changes: adding static resource embedding support and optimizing middleware configuration, which directly aligns with the PR's primary objectives of introducing rust-embed and restructuring static file serving.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch res

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request integrates rust-embed to serve static web assets directly from the binary in release builds, replacing the previous directory-based serving and custom AssetCache middleware. In debug mode, the server continues to serve files from the filesystem. Review feedback highlights a discrepancy between development and production asset paths, suggests simplifying path-stripping logic in the new handler, and identifies a regression regarding missing cache-control headers and ETag support for static assets.

Comment thread src/server.rs
.with_state(ctx);

#[cfg(debug_assertions)]
let app = app.fallback_service(ServeDir::new("webui"));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

开发环境下 ServeDir 使用的目录是 "webui",而生产环境下嵌入的目录是 "webui/dist"。如果前端构建产物位于 dist 目录中,开发环境下可能会因为路径不一致导致资源加载失败。此外,直接服务 "webui" 根目录可能会意外暴露前端源码或配置文件。建议统一使用 "webui/dist"

Suggested change
let app = app.fallback_service(ServeDir::new("webui"));
let app = app.fallback_service(ServeDir::new("webui/dist"));

Comment thread src/server.rs
Comment on lines +237 to +241
let path = uri
.path()
.trim_start_matches('/')
.strip_prefix("webui/")
.unwrap_or_else(|| uri.path().trim_start_matches('/'));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

这里的路径处理逻辑可以进一步简化,避免在 unwrap_or_else 中重复计算 trim_start_matches

    let path = uri.path().trim_start_matches('/');
    let path = path.strip_prefix("webui/").unwrap_or(path);

Comment thread src/server.rs
Comment on lines +245 to +252
match Assets::get(path) {
Some(content) => {
let mime = mime_guess::from_path(path).first_or_octet_stream();

([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
}
None => StatusCode::NOT_FOUND.into_response(),
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

相比于之前的 AssetCache 实现,新的 static_handler 丢失了对 /assets/ 路径的 immutable 缓存设置,且缺乏 ETag 支持。这会导致生产环境下静态资源的加载效率下降。建议恢复缓存策略,并利用 rust-embed 提供的哈希值添加 ETag 响应头以支持协商缓存。

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/server.rs`:
- Around line 245-252: The static asset handler that calls Assets::get(path)
currently only sets CONTENT_TYPE and dropped the previous AssetCache behavior;
restore Cache-Control handling by adding header::CACHE_CONTROL alongside
header::CONTENT_TYPE in the response: for fingerprinted asset paths (e.g., paths
under "assets/" or matching your fingerprint pattern) set a long-lived immutable
policy like "public, max-age=31536000, immutable", and for index.html set a
short/no-cache policy like "no-cache, must-revalidate" (or similar). Update the
code that builds the response (the branch that returns ([(header::CONTENT_TYPE,
...)], content.data).into_response()) to include the appropriate CACHE_CONTROL
header based on the path, using header::CACHE_CONTROL and the existing mime
variable to locate the change.
- Around line 245-252: When Assets::get(path) returns None currently you return
NOT_FOUND and break SPA deep links; change the fallback so the handler tries
Assets::get("index.html") and serves that content (with a text/html
Content-Type) when a static file isn't found, and only return NOT_FOUND if
index.html is also missing. Update the match branch that uses Assets::get(path)
to attempt Assets::get("index.html") on None, preserve existing mime handling
for real files (mime_guess::from_path(path)), and explicitly set the
Content-Type for the index fallback to "text/html" so the Vue router can handle
client-side routes.
- Around line 86-91: The SPA fallback is broken: in dev you use
ServeDir::new("webui") while in production static_handler uses
strip_prefix("webui/") and only serves index.html for empty paths, returning 404
for unknown routes and missing Cache-Control headers. Fix static_handler: remove
the strip_prefix("webui/") asymmetry so paths are handled the same as dev, when
Assets::get(path) returns None fall back to serving "index.html" from
Assets::get("index.html") (so client-side routes resolve) instead of returning
StatusCode::NOT_FOUND, and add appropriate Cache-Control headers (e.g., long TTL
for hashed assets, no-cache for index.html) to the responses returned from
static_handler and wherever Assets::get(...) is used; keep ServeDir usage in
debug only if you intentionally want unbuilt sources or explicitly
document/proxy to a dev server.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 21411a43-18e3-44e8-a88f-5832695012fd

📥 Commits

Reviewing files that changed from the base of the PR and between b0866c1 and fa67160.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • Cargo.toml
  • pnpm-workspace.yaml
  • src/server.rs
  • src/sync.rs

Comment thread src/server.rs
Comment on lines +86 to +91
#[cfg(debug_assertions)]
let app = app.fallback_service(ServeDir::new("webui"));

#[cfg(not(debug_assertions))]
let app = app.fallback(static_handler);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

What is the conventional file layout for a Vite/SPA project's build output, and how do Rust servers typically serve dev vs production from the same URL paths?

💡 Result:

The conventional file layout for a Vite/SPA project's build output (from vite build) is a dist/ directory containing index.html at the root and static assets (JS, CSS, images, etc.) in an assets/ subdirectory[1][2]. This structure supports SPA routing by serving index.html as a fallback for client-side routes while delivering assets from exact paths. Rust servers (e.g., Axum with tower-http or Actix-web) typically serve dev vs production from the same URL paths using conditional logic based on cfg!(debug_assertions) or environment variables[3][4][5][6]: - Development: Proxy non-API requests to Vite's dev server (usually localhost:5173) for HMR and on-demand serving. Crates like vite-actix automate this for Actix-web by proxying /assets/..., /node_modules/..., and unmatched routes when debug_assertions is true[4][7]. For Axum, spawn Vite's dev server and use a proxy service[5]. - Production: Serve static files from the dist/ directory using tower_http::services::ServeDir (Axum) or actix_files::Files (Actix), mounted at / with SPA fallback to index.html for unknown paths[8][9][10][6]. Example for Axum: use tower_http::services::ServeDir; let static_files = get_service(ServeDir::new("dist") .append_index_html_on_directories(true) .fallback_service(ServeFile::new("dist/index.html"))) .handle_error(...); Router::new() .nest("/api", api_routes) .fallback(static_files) This ensures /assets/app-abc.js serves the asset, /user/123 falls back to index.html (SPA router handles), and paths match Vite's output exactly in both modes[6][11]. Embed assets with rust-embed for single-binary deploys[8][3].

Citations:


🏁 Script executed:

cat -n src/server.rs | sed -n '80,260p'

Repository: KarinJS/mirror

Length of output: 5735


🏁 Script executed:

fd -type f webui | head -20

Repository: KarinJS/mirror

Length of output: 229


🏁 Script executed:

grep -r "strip_prefix\|webui/" src/server.rs -B 3 -A 3

Repository: KarinJS/mirror

Length of output: 416


🏁 Script executed:

fd webui --type d

Repository: KarinJS/mirror

Length of output: 65


🏁 Script executed:

ls -la webui/ 2>/dev/null || echo "webui directory not found"

Repository: KarinJS/mirror

Length of output: 782


🏁 Script executed:

ls -la webui/dist/ 2>/dev/null || echo "webui/dist directory not found"

Repository: KarinJS/mirror

Length of output: 89


🏁 Script executed:

cat webui/vite.config.ts

Repository: KarinJS/mirror

Length of output: 821


🏁 Script executed:

git log --oneline -n 20 -- src/server.rs | head -10

Repository: KarinJS/mirror

Length of output: 99


🏁 Script executed:

grep -r "5175\|vite\|dev" src/ --include="*.rs" | head -20

Repository: KarinJS/mirror

Length of output: 149


SPA fallback broken in production; dev/prod file paths diverge unnecessarily.

In cfg(debug_assertions) the fallback serves from the webui/ source directory (containing raw index.html and TypeScript sources). In release, assets are embedded from webui/dist/ (the built output). This means the same request path serves different source files and different directory structures across builds.

More critically, the static handler on lines 245–252 only serves index.html for an empty path (line 243) and returns StatusCode::NOT_FOUND for any unmatched route. SPAs require fallback to index.html for unknown paths so the client-side router can handle navigation. Without this, any route not explicitly matched as a static asset returns 404—breaking client-side routing (e.g., /user/123 will not resolve to the app).

Additionally, strip_prefix("webui/") on line 240 allows requests at /webui/... to be served in production but has no equivalent in dev, creating an unintended asymmetry that could collide with future routes under /webui.

Recommendations:

  1. Fix SPA fallback: Make the static handler serve index.html as a fallback for any 404 response (not just empty paths).
  2. Unify paths: Remove the strip_prefix("webui/") logic unless /webui/* routing is intentional and documented.
  3. Add cache headers: Embed assets should include Cache-Control headers (e.g., long TTL for hashed assets, no-cache for index.html).
  4. Consider dev alignment: If dev is meant to proxy to Vite's dev server, configure that explicitly rather than serving raw source files.
Relevant code
// Development: serves from source directory
#[cfg(debug_assertions)]
let app = app.fallback_service(ServeDir::new("webui"));

// Production: embedded from build output, but missing SPA fallback
#[cfg(not(debug_assertions))]
let app = app.fallback(static_handler);

async fn static_handler(uri: Uri) -> impl IntoResponse {
    // ... 
    let path = uri.path()
        .trim_start_matches('/')
        .strip_prefix("webui/")
        .unwrap_or_else(|| uri.path().trim_start_matches('/'));
    
    // Only serves index.html if path is empty; doesn't fallback for unknown routes
    let path = if path.is_empty() { "index.html" } else { path };
    
    match Assets::get(path) {
        Some(content) => (/* ... no cache headers ... */).into_response(),
        None => StatusCode::NOT_FOUND.into_response(),  // Missing SPA fallback here
    }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server.rs` around lines 86 - 91, The SPA fallback is broken: in dev you
use ServeDir::new("webui") while in production static_handler uses
strip_prefix("webui/") and only serves index.html for empty paths, returning 404
for unknown routes and missing Cache-Control headers. Fix static_handler: remove
the strip_prefix("webui/") asymmetry so paths are handled the same as dev, when
Assets::get(path) returns None fall back to serving "index.html" from
Assets::get("index.html") (so client-side routes resolve) instead of returning
StatusCode::NOT_FOUND, and add appropriate Cache-Control headers (e.g., long TTL
for hashed assets, no-cache for index.html) to the responses returned from
static_handler and wherever Assets::get(...) is used; keep ServeDir usage in
debug only if you intentionally want unbuilt sources or explicitly
document/proxy to a dev server.

Comment thread src/server.rs
Comment on lines +245 to +252
match Assets::get(path) {
Some(content) => {
let mime = mime_guess::from_path(path).first_or_octet_stream();

([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
}
None => StatusCode::NOT_FOUND.into_response(),
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cache-Control regression for embedded static assets.

The removed AssetCache middleware previously added long-lived immutable Cache-Control headers for /assets/ (per the PR summary). The new handler emits only Content-Type, so hashed/fingerprinted bundles under /assets/* will no longer be cached aggressively by browsers/CDNs — every navigation re-downloads the JS/CSS, hurting cold-cache behavior and bandwidth.

Consider re-attaching cache headers in static_handler for fingerprinted asset paths (and a short/no-cache for index.html):

♻️ Suggested cache policy
     match Assets::get(path) {
         Some(content) => {
             let mime = mime_guess::from_path(path).first_or_octet_stream();
-
-            ([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
+            let cache_control = if path.starts_with("assets/") {
+                "public, max-age=31536000, immutable"
+            } else {
+                "no-cache"
+            };
+            (
+                [
+                    (header::CONTENT_TYPE, mime.as_ref()),
+                    (header::CACHE_CONTROL, cache_control),
+                ],
+                content.data,
+            )
+                .into_response()
         }
         None => StatusCode::NOT_FOUND.into_response(),
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
match Assets::get(path) {
Some(content) => {
let mime = mime_guess::from_path(path).first_or_octet_stream();
([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
}
None => StatusCode::NOT_FOUND.into_response(),
}
match Assets::get(path) {
Some(content) => {
let mime = mime_guess::from_path(path).first_or_octet_stream();
let cache_control = if path.starts_with("assets/") {
"public, max-age=31536000, immutable"
} else {
"no-cache"
};
(
[
(header::CONTENT_TYPE, mime.as_ref()),
(header::CACHE_CONTROL, cache_control),
],
content.data,
)
.into_response()
}
None => StatusCode::NOT_FOUND.into_response(),
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server.rs` around lines 245 - 252, The static asset handler that calls
Assets::get(path) currently only sets CONTENT_TYPE and dropped the previous
AssetCache behavior; restore Cache-Control handling by adding
header::CACHE_CONTROL alongside header::CONTENT_TYPE in the response: for
fingerprinted asset paths (e.g., paths under "assets/" or matching your
fingerprint pattern) set a long-lived immutable policy like "public,
max-age=31536000, immutable", and for index.html set a short/no-cache policy
like "no-cache, must-revalidate" (or similar). Update the code that builds the
response (the branch that returns ([(header::CONTENT_TYPE, ...)],
content.data).into_response()) to include the appropriate CACHE_CONTROL header
based on the path, using header::CACHE_CONTROL and the existing mime variable to
locate the change.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether webui/dist is an SPA bundle by inspecting webui's build config / index.html
fd -HI -t f -d 4 'index.html|vite.config.*|package.json' webui 2>/dev/null | head -50
fd -HI -t f -d 3 'package.json' webui 2>/dev/null | xargs -I {} sh -c 'echo "=== {} ==="; cat {}'

Repository: KarinJS/mirror

Length of output: 926


🏁 Script executed:

# Get context around the Assets handler
wc -l src/server.rs
head -300 src/server.rs | tail -100

Repository: KarinJS/mirror

Length of output: 2603


🏁 Script executed:

# Search for the full route/router configuration
rg -n 'Assets::get|router|route|fallback' src/server.rs -A 2 -B 2

Repository: KarinJS/mirror

Length of output: 2211


Implement SPA fallback — deep-linked routes will 404 in production.

The webui directory contains a Vue 3 + Vite SPA (confirmed from vite.config.ts and package.json). When users navigate directly to routes like /settings or /profile/123, those paths have no corresponding files in webui/dist/, so Assets::get(path) returns None and the handler returns StatusCode::NOT_FOUND. This breaks page reloads and direct navigation to client-side routes.

Apply the SPA fallback pattern: when a file is not found, serve index.html instead so the Vue router can handle client-side navigation.

♻️ Proposed fix: fall back to index.html for non-file paths
     match Assets::get(path) {
         Some(content) => {
             let mime = mime_guess::from_path(path).first_or_octet_stream();
 
             ([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
         }
-        None => StatusCode::NOT_FOUND.into_response(),
+        None => match Assets::get("index.html") {
+            Some(index) => (
+                [(header::CONTENT_TYPE, "text/html; charset=utf-8")],
+                index.data,
+            )
+                .into_response(),
+            None => StatusCode::NOT_FOUND.into_response(),
+        },
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
match Assets::get(path) {
Some(content) => {
let mime = mime_guess::from_path(path).first_or_octet_stream();
([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
}
None => StatusCode::NOT_FOUND.into_response(),
}
match Assets::get(path) {
Some(content) => {
let mime = mime_guess::from_path(path).first_or_octet_stream();
([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
}
None => match Assets::get("index.html") {
Some(index) => (
[(header::CONTENT_TYPE, "text/html; charset=utf-8")],
index.data,
)
.into_response(),
None => StatusCode::NOT_FOUND.into_response(),
},
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server.rs` around lines 245 - 252, When Assets::get(path) returns None
currently you return NOT_FOUND and break SPA deep links; change the fallback so
the handler tries Assets::get("index.html") and serves that content (with a
text/html Content-Type) when a static file isn't found, and only return
NOT_FOUND if index.html is also missing. Update the match branch that uses
Assets::get(path) to attempt Assets::get("index.html") on None, preserve
existing mime handling for real files (mime_guess::from_path(path)), and
explicitly set the Content-Type for the index fallback to "text/html" so the Vue
router can handle client-side routes.

@sj817 sj817 merged commit ef479a7 into main May 8, 2026
4 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants