diff --git a/server-rs/crates/platform-tripo/src/common/config.rs b/server-rs/crates/platform-tripo/src/common/config.rs index 66bc0548b..0fa12c18f 100644 --- a/server-rs/crates/platform-tripo/src/common/config.rs +++ b/server-rs/crates/platform-tripo/src/common/config.rs @@ -51,6 +51,9 @@ impl TripoSettings { /// 校验配置自洽。 /// + /// `base_url` 必须是绝对的 `http(s)` 地址:只会「非空」挡不住 `example.com` 这类少了 + /// scheme 的写法,那种值要等到真正发请求时才失败,报错指向传输层而不是配置本身。 + /// /// 字段都是公开的,调用方可以用结构体字面量绕过 [`TripoSettings::new`]; /// [`crate::TripoProviderClient::new`] 会再校验一次,保证真正发起请求的那一端 /// 拿到的永远是合法配置。 @@ -67,6 +70,23 @@ impl TripoSettings { message: "base_url must not be blank".to_string(), }); } + let base_url = + url::Url::parse(self.base_url.trim()).map_err(|error| TripoError::Configuration { + field: "base_url", + message: format!("base_url must be an absolute URL: {error}"), + })?; + if !matches!(base_url.scheme(), "http" | "https") { + return Err(TripoError::Configuration { + field: "base_url", + message: "base_url must use http or https".to_string(), + }); + } + if base_url.host_str().is_none() { + return Err(TripoError::Configuration { + field: "base_url", + message: "base_url must include a host".to_string(), + }); + } if self.request_timeout.is_zero() { return Err(TripoError::Configuration { field: "request_timeout", @@ -127,6 +147,29 @@ mod tests { } } + #[test] + fn base_url_must_be_an_absolute_http_url() { + for (label, base_url) in [ + ("缺 scheme", "openapi.tripo3d.com/v3"), + ("非 http(s)", "ftp://openapi.tripo3d.com/v3"), + ("不是 URL", "not-a-url"), + ] { + let error = settings(Duration::from_secs(1), "key", base_url) + .validate() + .expect_err(&format!("{label} 必须被拒绝")); + assert!(matches!(error, TripoError::Configuration { .. }), "{error}"); + } + + // 前后带空白的合法地址仍然放行:先 trim 再校验,与取值口径一致。 + settings( + Duration::from_secs(1), + "key", + " https://openapi.tripo3d.com/v3 ", + ) + .validate() + .expect("带空白的合法地址应可校验通过"); + } + #[test] fn new_returns_ready_to_use_settings() { let settings = TripoSettings::new(