Conversation
- 引入 rust-embed 依赖以支持编译时静态资源嵌入 - 移除自定义 AssetCache 中间件,改用 rust-embed 实现静态文件服务 - 配置开发环境使用 ServeDir,生产环境使用嵌入式静态资源 - 简化 HTTP 客户端初始化和服务构建逻辑 - 优化路由配置和错误处理流程
📝 WalkthroughWalkthroughThis PR integrates the ChangesEmbedded Asset Serving Integration
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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.
| .with_state(ctx); | ||
|
|
||
| #[cfg(debug_assertions)] | ||
| let app = app.fallback_service(ServeDir::new("webui")); |
There was a problem hiding this comment.
| let path = uri | ||
| .path() | ||
| .trim_start_matches('/') | ||
| .strip_prefix("webui/") | ||
| .unwrap_or_else(|| uri.path().trim_start_matches('/')); |
| 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(), | ||
| } |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (4)
Cargo.tomlpnpm-workspace.yamlsrc/server.rssrc/sync.rs
| #[cfg(debug_assertions)] | ||
| let app = app.fallback_service(ServeDir::new("webui")); | ||
|
|
||
| #[cfg(not(debug_assertions))] | ||
| let app = app.fallback(static_handler); | ||
|
|
There was a problem hiding this comment.
🧩 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:
- 1: https://main.vitejs.dev/config/build-options.html
- 2: https://vite.dev/config/build-options
- 3: https://nguyenhuythanh.com/posts/rust-backend-spa/
- 4: https://docs.rs/vite-actix/latest/vite_actix/
- 5: https://docs.rs/crate/vite-rs-axum-0-8/0.2.1
- 6: https://docs.rs/tower-http/latest/tower_http/services/struct.ServeDir.html
- 7: https://crates.io/crates/vite-actix
- 8: https://stackoverflow.com/questions/73464479/how-to-host-spa-files-and-embed-too-with-axum-and-rust-embed
- 9: How to host SPA files and embed files too in executable? tokio-rs/axum#1309
- 10: https://stackoverflow.com/questions/73169494/statically-deploy-sveltekit-to-actix
- 11: Add
ServeDir::fallbacktower-rs/tower-http#243
🏁 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 -20Repository: KarinJS/mirror
Length of output: 229
🏁 Script executed:
grep -r "strip_prefix\|webui/" src/server.rs -B 3 -A 3Repository: KarinJS/mirror
Length of output: 416
🏁 Script executed:
fd webui --type dRepository: 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.tsRepository: KarinJS/mirror
Length of output: 821
🏁 Script executed:
git log --oneline -n 20 -- src/server.rs | head -10Repository: KarinJS/mirror
Length of output: 99
🏁 Script executed:
grep -r "5175\|vite\|dev" src/ --include="*.rs" | head -20Repository: 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:
- Fix SPA fallback: Make the static handler serve
index.htmlas a fallback for any 404 response (not just empty paths). - Unify paths: Remove the
strip_prefix("webui/")logic unless/webui/*routing is intentional and documented. - Add cache headers: Embed assets should include
Cache-Controlheaders (e.g., long TTL for hashed assets, no-cache forindex.html). - 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.
| 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(), | ||
| } |
There was a problem hiding this comment.
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.
| 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.
🧩 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 -100Repository: 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 2Repository: 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.
| 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.
Summary by CodeRabbit
Chores
Refactor