mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
fix: serve vite public assets
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Generated
+27
@@ -1112,6 +1112,12 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-range-header"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
@@ -1496,6 +1502,16 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -2839,6 +2855,11 @@ dependencies = [
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"http-range-header",
|
||||
"httpdate",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -2994,6 +3015,12 @@ version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.18"
|
||||
|
||||
+2
-2
@@ -37,7 +37,8 @@ tar = "0.4.44"
|
||||
thiserror = "2.0.18"
|
||||
tokio = { version = "1.52.3", features = ["macros", "rt-multi-thread", "signal", "fs", "time", "sync", "net", "test-util"] }
|
||||
tokio-tungstenite = { version = "0.28.0", features = ["proxy", "rustls-tls-native-roots"] }
|
||||
tower-http = { version = "0.6.11", features = ["trace", "cors", "request-id", "timeout"] }
|
||||
tower = { version = "0.5.3", features = ["util"] }
|
||||
tower-http = { version = "0.6.11", features = ["trace", "cors", "request-id", "timeout", "fs"] }
|
||||
tracing = "0.1.44"
|
||||
tracing-appender = "0.2.5"
|
||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter", "json"] }
|
||||
@@ -46,7 +47,6 @@ uuid = { version = "1.23.3", features = ["v4", "v7", "serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.27.0"
|
||||
tower = { version = "0.5.3", features = ["util"] }
|
||||
wiremock = "0.6.5"
|
||||
|
||||
[patch.crates-io]
|
||||
|
||||
+28
-66
@@ -1,41 +1,32 @@
|
||||
use std::{
|
||||
path::{Component, Path, PathBuf},
|
||||
sync::Arc,
|
||||
convert::Infallible,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{Path as AxumPath, State},
|
||||
http::{header, HeaderValue, StatusCode, Uri},
|
||||
http::{Request, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AssetState {
|
||||
dist_dir: Arc<PathBuf>,
|
||||
}
|
||||
use tower::service_fn;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
pub fn spa_router(dist_dir: impl AsRef<Path>) -> Router {
|
||||
let state = AssetState {
|
||||
dist_dir: Arc::new(dist_dir.as_ref().to_path_buf()),
|
||||
};
|
||||
let dist_dir = dist_dir.as_ref().to_path_buf();
|
||||
let index_file = dist_dir.join("index.html");
|
||||
let fallback = service_fn(move |request: Request<Body>| {
|
||||
let index_file = index_file.clone();
|
||||
async move { Ok::<_, Infallible>(spa_fallback_response(request, index_file).await) }
|
||||
});
|
||||
|
||||
Router::new()
|
||||
.route("/", get(serve_index))
|
||||
.route("/assets/{*path}", get(serve_asset))
|
||||
.fallback(serve_spa_fallback)
|
||||
.with_state(state)
|
||||
Router::new().fallback_service(ServeDir::new(dist_dir).fallback(fallback))
|
||||
}
|
||||
|
||||
async fn serve_index(State(state): State<AssetState>) -> Response {
|
||||
serve_file(&state.dist_dir.join("index.html")).await
|
||||
}
|
||||
|
||||
async fn serve_spa_fallback(State(state): State<AssetState>, uri: Uri) -> Response {
|
||||
if is_api_path(uri.path()) {
|
||||
async fn spa_fallback_response(request: Request<Body>, index_file: PathBuf) -> Response {
|
||||
let path = request.uri().path();
|
||||
if is_api_path(path) {
|
||||
return (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(json!({
|
||||
@@ -47,61 +38,32 @@ async fn serve_spa_fallback(State(state): State<AssetState>, uri: Uri) -> Respon
|
||||
.into_response();
|
||||
}
|
||||
|
||||
serve_file(&state.dist_dir.join("index.html")).await
|
||||
if is_static_file_path(path) {
|
||||
return StatusCode::NOT_FOUND.into_response();
|
||||
}
|
||||
|
||||
serve_index_file(&index_file).await
|
||||
}
|
||||
|
||||
fn is_api_path(path: &str) -> bool {
|
||||
path == "/api" || path.starts_with("/api/") || path == "/v1" || path.starts_with("/v1/")
|
||||
}
|
||||
|
||||
async fn serve_asset(
|
||||
State(state): State<AssetState>,
|
||||
AxumPath(path): AxumPath<String>,
|
||||
) -> Response {
|
||||
let Some(relative_path) = safe_relative_path(&path) else {
|
||||
return StatusCode::NOT_FOUND.into_response();
|
||||
};
|
||||
serve_file(&state.dist_dir.join("assets").join(relative_path)).await
|
||||
}
|
||||
|
||||
async fn serve_file(path: &Path) -> Response {
|
||||
async fn serve_index_file(path: &Path) -> Response {
|
||||
let Ok(bytes) = tokio::fs::read(path).await else {
|
||||
return StatusCode::NOT_FOUND.into_response();
|
||||
};
|
||||
|
||||
let mut response = Response::new(Body::from(bytes));
|
||||
let headers = response.headers_mut();
|
||||
headers.insert(
|
||||
header::CONTENT_TYPE,
|
||||
content_type_for_path(&path.to_string_lossy()),
|
||||
response.headers_mut().insert(
|
||||
axum::http::header::CONTENT_TYPE,
|
||||
axum::http::HeaderValue::from_static("text/html; charset=utf-8"),
|
||||
);
|
||||
response
|
||||
}
|
||||
|
||||
fn content_type_for_path(path: &str) -> HeaderValue {
|
||||
if path.ends_with(".html") || path == "/" {
|
||||
HeaderValue::from_static("text/html; charset=utf-8")
|
||||
} else if path.ends_with(".js") {
|
||||
HeaderValue::from_static("text/javascript; charset=utf-8")
|
||||
} else if path.ends_with(".css") {
|
||||
HeaderValue::from_static("text/css; charset=utf-8")
|
||||
} else if path.ends_with(".json") {
|
||||
HeaderValue::from_static("application/json")
|
||||
} else if path.ends_with(".svg") {
|
||||
HeaderValue::from_static("image/svg+xml")
|
||||
} else {
|
||||
HeaderValue::from_static("application/octet-stream")
|
||||
}
|
||||
}
|
||||
|
||||
fn safe_relative_path(path: &str) -> Option<PathBuf> {
|
||||
let mut sanitized = PathBuf::new();
|
||||
for component in Path::new(path).components() {
|
||||
match component {
|
||||
Component::Normal(segment) => sanitized.push(segment),
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
|
||||
(!sanitized.as_os_str().is_empty()).then_some(sanitized)
|
||||
fn is_static_file_path(path: &str) -> bool {
|
||||
Path::new(path.trim_start_matches('/'))
|
||||
.extension()
|
||||
.is_some()
|
||||
}
|
||||
|
||||
@@ -22,6 +22,11 @@ async fn server_router_should_serve_frontend_assets_without_shadowing_api_routes
|
||||
"<!doctype html><main id=\"app\">Codex Proxy Admin</main>",
|
||||
)
|
||||
.expect("index should be written");
|
||||
std::fs::write(
|
||||
dist.join("favicon.svg"),
|
||||
"<svg><title>Codex Proxy</title></svg>",
|
||||
)
|
||||
.expect("favicon should be written");
|
||||
std::fs::write(dist.join("assets").join("app.js"), "window.__asset = true;")
|
||||
.expect("asset should be written");
|
||||
let db = dir.path().join("assets-routes.sqlite");
|
||||
@@ -29,6 +34,21 @@ async fn server_router_should_serve_frontend_assets_without_shadowing_api_routes
|
||||
let (app, _pool) = router_with_dist_and_database(&dist, &database_url).await;
|
||||
|
||||
let index = app.clone().oneshot(request("/")).await.expect("index");
|
||||
let route_fallback = app
|
||||
.clone()
|
||||
.oneshot(request("/dashboard"))
|
||||
.await
|
||||
.expect("route fallback");
|
||||
let favicon = app
|
||||
.clone()
|
||||
.oneshot(request("/favicon.svg"))
|
||||
.await
|
||||
.expect("favicon");
|
||||
let missing_static = app
|
||||
.clone()
|
||||
.oneshot(request("/missing.svg"))
|
||||
.await
|
||||
.expect("missing static response");
|
||||
let asset = app
|
||||
.clone()
|
||||
.oneshot(request("/assets/app.js"))
|
||||
@@ -45,12 +65,23 @@ async fn server_router_should_serve_frontend_assets_without_shadowing_api_routes
|
||||
.expect("health response");
|
||||
|
||||
assert_eq!(index.status(), StatusCode::OK);
|
||||
assert_eq!(route_fallback.status(), StatusCode::OK);
|
||||
assert_eq!(favicon.status(), StatusCode::OK);
|
||||
assert_eq!(missing_static.status(), StatusCode::NOT_FOUND);
|
||||
assert_eq!(asset.status(), StatusCode::OK);
|
||||
assert_eq!(api.status(), StatusCode::UNAUTHORIZED);
|
||||
assert_eq!(health.status(), StatusCode::NO_CONTENT);
|
||||
assert_eq!(
|
||||
favicon.headers().get(header::CONTENT_TYPE),
|
||||
Some(&header::HeaderValue::from_static("image/svg+xml"))
|
||||
);
|
||||
assert_no_static_policy_headers(&index);
|
||||
assert_no_static_policy_headers(&route_fallback);
|
||||
assert_no_static_policy_headers(&favicon);
|
||||
assert_no_static_policy_headers(&asset);
|
||||
assert_body_contains(index, "Codex Proxy Admin").await;
|
||||
assert_body_contains(route_fallback, "Codex Proxy Admin").await;
|
||||
assert_body_contains(favicon, "Codex Proxy").await;
|
||||
assert_body_contains(asset, "__asset").await;
|
||||
assert_eq!(response_json(api).await["code"], Value::from(40101));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user