Skip to main content

xyz_rust/httpapi/
mod.rs

1// httpapi 是 HTTP 前端,构建于 axum(Rust 生态的事实标准 HTTP 栈——
2// 官方 rmcp 的 streamable HTTP 服务同栈,serve 模式把 /mcp 挂同一端口时
3// 零桥接)。职责对齐 Go 版:
4//
5//   - HTTPHints.method + path 定义路由;path 模板 {name} 映射到绑定了
6//     http="path" 的字段;
7//   - 未标注位置(或标注 http:"query")的字段默认从 query 绑定;
8//     http:"header"(http_name)从请求头绑定;JSON body 合并为入参基底;
9//   - 响应是裸 JSON(无信封),与 CLI --json 同形;错误经共享错误分类学
10//     映射为 HTTP 状态码;
11//   - GET /healthz 探活;GET /openapi.json 暴露与 MCP inputSchema 同源的
12//     OpenAPI 3 文档。
13//
14// 没有 HTTP hints 的条目不路由。Go 版声称「纯标准库」——Rust std 无
15// HTTP 服务器,这里的对等物是 axum(标注在 README 的差异节)。
16
17pub mod middleware;
18pub mod openapi;
19
20#[cfg(test)]
21mod httpapi_test;
22
23use std::collections::HashSet;
24use std::sync::Arc;
25
26use axum::extract::Request;
27use axum::http::{Method, StatusCode, header};
28use axum::response::{IntoResponse, Response};
29use axum::routing::{Router, delete, get, head, options, patch, post, put};
30use serde_json::{Map, Value};
31
32use crate::config::Config;
33use crate::ctx::Ctx;
34use crate::errors;
35use crate::registry::Registry;
36use crate::spec::entry::Entry;
37
38/// 请求体上限 1MiB(Go maxBodyBytes 同值)。
39pub const MAX_BODY_BYTES: usize = 1 << 20;
40
41/// Handler 构建 serve 模式的整表路由(REST + /healthz + /openapi.json)。
42/// method+path 冲突是注册期错误(Go 用 recover 包 mux panic 转错误,
43/// 这里显式前置检查,行为等价)。
44pub fn router(reg: &Registry, ctx: Arc<Ctx>) -> errors::Result<Router> {
45    router_with(
46        reg,
47        ctx,
48        std::collections::HashMap::new(),
49        false,
50        false,
51        &crate::cli::bin_name(),
52        crate::version::version(),
53    )
54}
55
56/// 带通道级默认参数的路由器(serve --default k=v:缺席键补上)。
57pub(crate) fn router_with(
58    reg: &Registry,
59    ctx: Arc<Ctx>,
60    defaults: std::collections::HashMap<String, String>,
61    no_server_headers: bool,
62    allow_unknown_query: bool,
63    app_name: &str,
64    app_version: &str,
65) -> errors::Result<Router> {
66    // 装配期 axum 可能因路由冲突 panic(例如用户命令占用 /healthz)——
67    // 捕获并转为注册期错误(对齐 Go registerSafe 的 recover)。
68    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
69        router_inner(
70            reg,
71            ctx,
72            defaults,
73            no_server_headers,
74            allow_unknown_query,
75            app_name,
76            app_version,
77        )
78    }))
79    .unwrap_or_else(|p| {
80        let msg = p
81            .downcast_ref::<&str>()
82            .map(|s| s.to_string())
83            .or_else(|| p.downcast_ref::<String>().cloned())
84            .unwrap_or_else(|| "route registration panic".to_string());
85        Err(errors::Error::new(
86            errors::Kind::Internal,
87            format!("httpapi: route conflicts with an existing route ({msg})"),
88        ))
89    })
90}
91
92fn router_inner(
93    reg: &Registry,
94    ctx: Arc<Ctx>,
95    defaults: std::collections::HashMap<String, String>,
96    no_server_headers: bool,
97    allow_unknown_query: bool,
98    app_name: &str,
99    app_version: &str,
100) -> errors::Result<Router> {
101    let mut r: Router = Router::new();
102    let mut seen: HashSet<(String, String)> = HashSet::new();
103    for e in reg.all() {
104        if e.http.skip || e.cli.daemon {
105            continue; // 通道层面整体移除;daemon 只属于 CLI
106        }
107        if e.http.path.is_empty() {
108            continue; // 该命令没有声明 HTTP 路由(CLI/MCP 专用)
109        }
110        // 空 method = 同时注册 GET 与 POST(spec §11.1 默认;GET 绑 query、
111        // POST 绑 body+query,同一 handler);method 可为逗号分隔多方法。
112        for method in http_methods(&e) {
113            if !seen.insert((method.clone(), e.http.path.clone())) {
114                return Err(errors::Error::new(
115                    errors::Kind::Internal,
116                    format!(
117                        "httpapi: route {:?} {:?} conflicts with an existing route",
118                        method, e.http.path
119                    ),
120                ));
121            }
122            let h = handle_entry_c(
123                e.clone(),
124                Arc::clone(&ctx),
125                Arc::new(defaults.clone()),
126                no_server_headers,
127                allow_unknown_query,
128            );
129            let mr = method_router(&method, h)?;
130            r = r.route(&e.http.path, mr);
131        }
132    }
133    r = r
134        .route("/healthz", get(healthz))
135        .route(
136            "/openapi.json",
137            get(openapi::openapi_handler(reg, app_name, app_version)),
138        )
139        // 未匹配路由的统一 404(对齐 Go:{"error": <本地化 not found>},
140        // 遵循 §11.7 的逐请求语言)。
141        .fallback(missing_route);
142    Ok(r)
143}
144
145/// 命令的注册方法集(spec §11.1):空 method → GET+POST 默认;否则按逗号
146/// 分隔(trim、忽略空项)。router 与 openapi 生成共用。
147pub(crate) fn http_methods(e: &crate::spec::Entry) -> Vec<String> {
148    let m = e.http.method.trim();
149    if m.is_empty() {
150        return vec!["GET".to_string(), "POST".to_string()];
151    }
152    // 逗号分隔、大小写归一为大写(对齐 Go httpMethods 的 ToUpper)。
153    m.split(',')
154        .map(|s| s.trim().to_uppercase())
155        .filter(|s| !s.is_empty())
156        .collect()
157}
158
159/// 未匹配路由的 404:JSON 错误体 + 请求语言本地化。
160async fn missing_route(req: Request) -> Response {
161    let lang = req
162        .headers()
163        .get(header::ACCEPT_LANGUAGE)
164        .and_then(|v| v.to_str().ok())
165        .and_then(crate::lang::parse_accept_language);
166    let msg = match lang {
167        Some(l) => crate::lang::t_lang(l, "http.err_not_found"),
168        None => crate::lang::t("http.err_not_found"),
169    };
170    write_error(StatusCode::NOT_FOUND, &msg)
171}
172
173fn method_router<H, T>(method: &str, h: H) -> errors::Result<axum::routing::MethodRouter>
174where
175    H: axum::handler::Handler<T, ()>,
176    T: 'static,
177{
178    // axum 的 MethodRouter 覆盖常规动词;其余动词(Go net/http 的任意
179    // 方法)在注册期给出清晰错误。
180    Ok(match method {
181        "GET" => get(h),
182        "POST" => post(h),
183        "PUT" => put(h),
184        "PATCH" => patch(h),
185        "DELETE" => delete(h),
186        "HEAD" => head(h),
187        "OPTIONS" => options(h),
188        other => {
189            return Err(errors::Error::new(
190                errors::Kind::Internal,
191                format!("httpapi: unsupported HTTP method {other:?}"),
192            ));
193        }
194    })
195}
196
197/// 单条命令的完整绑定处理器(query/path/header/body + 错误映射),无
198/// 路由。挂到任意 axum Router 上(docs/adapters.md 有示例)。
199/// 轮询 Handler:axum 闭包 handler 的请求抽取元组带私有 ViaRequest
200/// 标记、无法具名,这里以自实现 Handler 的具名类型给出清晰公开 API
201/// (axum 文档支持的自定义 handler 路线)。
202#[derive(Clone)]
203pub struct EntryHandler {
204    pub(crate) entry: Arc<Entry>,
205    pub(crate) ctx: Arc<Ctx>,
206    pub(crate) defaults: Arc<std::collections::HashMap<String, String>>,
207    /// 关闭每路由的 X-XYZ-Command/X-XYZ-Duration-Ms(§11.6 单一开关)。
208    pub(crate) no_server_headers: bool,
209    /// 接受未知 query 参数(默认严格拒绝,spec §11.2)。
210    pub(crate) allow_unknown_query: bool,
211}
212
213impl axum::handler::Handler<(), ()> for EntryHandler {
214    type Future = std::pin::Pin<Box<dyn std::future::Future<Output = Response> + Send>>;
215    fn call(self, req: Request, _state: ()) -> Self::Future {
216        Box::pin(async move {
217            handle_request(
218                &self.entry,
219                &self.ctx,
220                &self.defaults,
221                self.no_server_headers,
222                self.allow_unknown_query,
223                req,
224            )
225            .await
226        })
227    }
228}
229
230pub fn handler_for(entry: Arc<Entry>) -> EntryHandler {
231    handle_entry_c(
232        entry,
233        Arc::new(Ctx::new()),
234        Arc::new(std::collections::HashMap::new()),
235        false,
236        false,
237    )
238}
239
240/// 入口处理器构建(serve 模式注入取消上下文)。
241fn handle_entry_c(
242    entry: Arc<Entry>,
243    ctx: Arc<Ctx>,
244    defaults: Arc<std::collections::HashMap<String, String>>,
245    no_server_headers: bool,
246    allow_unknown_query: bool,
247) -> EntryHandler {
248    EntryHandler {
249        entry,
250        ctx,
251        defaults,
252        no_server_headers,
253        allow_unknown_query,
254    }
255}
256
257async fn handle_request(
258    entry: &Arc<Entry>,
259    ctx: &Arc<Ctx>,
260    defaults: &std::collections::HashMap<String, String>,
261    no_server_headers: bool,
262    allow_unknown_query: bool,
263    req: Request,
264) -> Response {
265    let (parts, body) = req.into_parts();
266    let method = parts.method.clone();
267    let path = parts.uri.path().to_string();
268    let query = parts.uri.query().unwrap_or_default().to_string();
269    let headers = parts.headers.clone();
270
271    // 逐请求语言(spec §11.7):Accept-Language 命中受支持语言则用之,否则
272    // 回退进程默认;派生到请求 ctx 供 Invoke 管线、handler 与本地化框架
273    // 消息共用(§14 item7 的 language_from_ctx 读取点)。
274    let req_lang = headers
275        .get(header::ACCEPT_LANGUAGE)
276        .and_then(|v| v.to_str().ok())
277        .and_then(crate::lang::parse_accept_language);
278    let localize = |key: &str| match req_lang {
279        Some(l) => crate::lang::t_lang(l, key),
280        None => crate::lang::t(key),
281    };
282    let localize_tf = |key: &str, params: &[&str]| match req_lang {
283        Some(l) => crate::lang::tf_lang(l, key, params),
284        None => crate::lang::tf(key, params),
285    };
286
287    // 未知 query 参数默认严格拒绝(spec §11.2):静默忽略会让拼错或已移除
288    // 的参数看起来生效(客户端据此误判)。允许集 = 本路由以 query 绑定的
289    // 字段(含 §4.6 的 http_name 覆写名)。--xyz.allow-unknown-query 放宽。
290    if !allow_unknown_query && !query.is_empty() {
291        let mut allowed: Vec<&str> = Vec::new();
292        for f in &entry.root.children {
293            if f.skip || !matches!(f.http.location.as_str(), "" | "query") {
294                continue;
295            }
296            let n = http_name(f);
297            if !allowed.contains(&n) {
298                allowed.push(n);
299            }
300        }
301        let names = if allowed.is_empty() {
302            "—".to_string()
303        } else {
304            allowed.join(", ")
305        };
306        for (k, _) in parse_form_query(&query) {
307            if !allowed.contains(&k.as_str()) {
308                let msg = localize_tf("http.err_unknown_query", &[&k, &names]);
309                return write_coded_error(&errors::Error::new(errors::Kind::InvalidInput, msg));
310            }
311        }
312    }
313
314    let mut m: Map<String, Value> = Map::new();
315    // 铺底:HTTP 专属默认值(全局默认由 Invoke 补齐)。
316    for (k, v) in entry.http_defaults() {
317        m.insert(k, v);
318    }
319    // 通道级默认参数(serve --default k=v):只补缺席键。
320    for (k, v) in defaults.iter() {
321        if !m.contains_key(k) {
322            m.insert(k.clone(), Value::String(v.clone()));
323        }
324    }
325    // JSON body 作为基础入参(非 GET/HEAD 且带体时解析)。
326    let body_bytes: Vec<u8> = if method != Method::GET && method != Method::HEAD {
327        collect_body(body, MAX_BODY_BYTES).await
328    } else {
329        Vec::new()
330    };
331    if !body_bytes.is_empty() {
332        let json_declared = headers
333            .get(header::CONTENT_TYPE)
334            .and_then(|v| v.to_str().ok())
335            .map(|v| v.contains("application/json"))
336            .unwrap_or(false);
337        match serde_json::from_slice::<serde_json::Value>(&body_bytes) {
338            Ok(Value::Object(parsed)) => {
339                for (k, v) in parsed {
340                    m.insert(k, v);
341                }
342            }
343            Ok(_) => {
344                return write_error(StatusCode::BAD_REQUEST, &localize("http.err_invalid_json"));
345            }
346            Err(_) if json_declared => {
347                // 显式声明 application/json 却解析失败:严格 400。
348                return write_error(StatusCode::BAD_REQUEST, &localize("http.err_invalid_json"));
349            }
350            Err(_) => {
351                // 非 JSON 声明(如表单体)解析失败:交给 form 绑定,不提前判死。
352            }
353        }
354    }
355    // form 字段惰性解析:只在该条目确实有 http:"form" 字段时解析表单。
356    let mut form_parsed: Option<Vec<(String, String)>> = None;
357
358    for f in &entry.root.children {
359        if f.skip {
360            // json:"-" 的注入字段:header 值以 Rust 字段名为键送达。
361            if f.http.location == "header"
362                && let Some(v) = headers.get(http_name(f)).and_then(|v| v.to_str().ok())
363                && !v.is_empty()
364            {
365                m.insert(f.name.clone(), Value::String(v.to_string()));
366            }
367            continue;
368        }
369        match f.http.location.as_str() {
370            "" | "query" => {
371                // §4.6:http_name 覆写线上名(query 也适用,与 OpenAPI 一致)。
372                if let Some(vs) = query_values(&query, http_name(f)) {
373                    if is_string_slice(f) {
374                        m.insert(
375                            f.json_name.clone(),
376                            Value::Array(vs.iter().map(|s| Value::String(s.clone())).collect()),
377                        );
378                    } else if let Some(first) = vs.first() {
379                        m.insert(f.json_name.clone(), Value::String(first.clone()));
380                    }
381                }
382            }
383            "header" => {
384                if let Some(v) = headers.get(http_name(f)).and_then(|v| v.to_str().ok())
385                    && !v.is_empty()
386                {
387                    m.insert(f.json_name.clone(), Value::String(v.to_string()));
388                }
389            }
390            "path" => {
391                if let Some(v) = path_param(&entry.http.path, &path, http_name(f)) {
392                    m.insert(f.json_name.clone(), Value::String(v));
393                }
394            }
395            "form" => {
396                if form_parsed.is_none() {
397                    // 与上游一致:form 值仅来自请求体(已读字节,避免二读 body)。
398                    form_parsed = Some(crate::httpapi::parse_form_query(&String::from_utf8_lossy(
399                        &body_bytes,
400                    )));
401                }
402                if let Some((_, v)) = form_parsed
403                    .as_ref()
404                    .unwrap()
405                    .iter()
406                    .find(|(k, _)| k == http_name(f))
407                {
408                    m.insert(f.json_name.clone(), Value::String(v.clone()));
409                }
410            }
411            _ => {}
412        }
413    }
414
415    let per_req_ctx = match req_lang {
416        Some(l) => Arc::new(ctx.with_language(l.as_str())),
417        None => Arc::clone(ctx),
418    };
419    let started = std::time::Instant::now();
420    let mut resp = match (entry.invoke)(&per_req_ctx, &m) {
421        Ok(out) => {
422            let s = serde_json::to_string_pretty(&out).unwrap_or_else(|_| "null".to_string());
423            json_response(StatusCode::OK, &s)
424        }
425        Err(e) => write_coded_error(&e),
426    };
427    // §11.6 per-route 上下文头(身份/自定义头由 serve 的全局层写)。
428    if !no_server_headers {
429        let h = resp.headers_mut();
430        if let Ok(v) = header::HeaderValue::from_str(&entry.name) {
431            let _ = h.insert(header::HeaderName::from_static("x-xyz-command"), v);
432        }
433        let dur = started.elapsed().as_millis().to_string();
434        if let Ok(v) = header::HeaderValue::from_str(&dur) {
435            let _ = h.insert(header::HeaderName::from_static("x-xyz-duration-ms"), v);
436        }
437    }
438    resp
439}
440
441/// httpName 计算线上名:http_name 覆盖 > JSON 名 > Rust 字段名。
442pub(crate) fn http_name(f: &crate::spec::FieldMeta) -> &str {
443    if !f.http.name.is_empty() {
444        return f.http.name.as_str();
445    }
446    if !f.json_name.is_empty() {
447        return f.json_name.as_str();
448    }
449    f.name.as_str()
450}
451
452fn is_string_slice(f: &crate::spec::FieldMeta) -> bool {
453    matches!(f.kind, crate::spec::FieldKind::Slice)
454}
455
456/// 手写 query/form 解析(零第三方依赖的 urlencoded 子集):按 & 拆对、
457/// % 解码。
458pub(crate) fn parse_form_query(s: &str) -> Vec<(String, String)> {
459    s.split('&')
460        .filter(|p| !p.is_empty())
461        .map(|pair| match pair.split_once('=') {
462            Some((k, v)) => (pct_decode(k), pct_decode(v)),
463            None => (pct_decode(pair), String::new()),
464        })
465        .collect()
466}
467
468fn query_values(query: &str, key: &str) -> Option<Vec<String>> {
469    let pairs = parse_form_query(query);
470    let hit: Vec<String> = pairs
471        .iter()
472        .filter(|(k, _)| k == key)
473        .map(|(_, v)| v.clone())
474        .collect();
475    if hit.is_empty() { None } else { Some(hit) }
476}
477
478/// 按模板段摘出 {name} 路径参数(路由已由 axum 匹配,这里只取值)。
479fn path_param(template: &str, path: &str, name: &str) -> Option<String> {
480    let tsegs: Vec<&str> = template.split('/').collect();
481    let psegs: Vec<&str> = path.split('/').collect();
482    if tsegs.len() != psegs.len() {
483        return None;
484    }
485    for (t, p) in tsegs.iter().zip(psegs.iter()) {
486        if let Some(inner) = t.strip_prefix('{').and_then(|s| s.strip_suffix('}'))
487            && inner == name
488        {
489            return Some(pct_decode(p));
490        }
491    }
492    None
493}
494
495fn pct_decode(s: &str) -> String {
496    let bytes = s.as_bytes();
497    let mut out = Vec::with_capacity(bytes.len());
498    let mut i = 0;
499    while i < bytes.len() {
500        if bytes[i] == b'%' && i + 2 < bytes.len() + 1 {
501            let h = hex_val(bytes.get(i + 1));
502            let l = hex_val(bytes.get(i + 2));
503            if let (Some(h), Some(l)) = (h, l) {
504                out.push(h * 16 + l);
505                i += 3;
506                continue;
507            }
508        }
509        if bytes[i] == b'+' {
510            out.push(b' ');
511        } else {
512            out.push(bytes[i]);
513        }
514        i += 1;
515    }
516    String::from_utf8_lossy(&out).into_owned()
517}
518
519fn hex_val(b: Option<&u8>) -> Option<u8> {
520    match b.copied()? {
521        b'0'..=b'9' => Some(b.unwrap() - b'0'),
522        b'a'..=b'f' => Some(b.unwrap() - b'a' + 10),
523        b'A'..=b'F' => Some(b.unwrap() - b'A' + 10),
524        _ => None,
525    }
526}
527
528/// spec §8.6:HTTP 错误体 = 共享错误对象(紧凑 JSON),状态码遵循
529/// §8.5 的显式 status 覆盖,否则按 Kind 推导。
530fn write_coded_error(e: &errors::Error) -> Response {
531    let status =
532        StatusCode::from_u16(errors::error_status(e)).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
533    let mut body = errors::error_body(e);
534    if body.error.is_empty() {
535        body.error = status.canonical_reason().unwrap_or("error").to_string();
536    }
537    let s = serde_json::to_string(&body).unwrap_or_else(|_| "{\"error\":\"\"}".to_string());
538    json_response(status, &s)
539}
540
541fn write_error(status: StatusCode, msg: &str) -> Response {
542    let msg = if msg.is_empty() {
543        status.canonical_reason().unwrap_or("error").to_string()
544    } else {
545        msg.to_string()
546    };
547    // Go writeError:紧凑 JSON(结果才走 pretty)。
548    let body = serde_json::to_string(&serde_json::json!({ "error": msg }))
549        .unwrap_or_else(|_| "{\"error\":\"\"}".to_string());
550    json_response(status, &body)
551}
552
553fn json_response(status: StatusCode, body: &str) -> Response {
554    let mut b = body.to_string();
555    b.push('\n');
556    (
557        status,
558        [(header::CONTENT_TYPE, "application/json; charset=utf-8")],
559        b,
560    )
561        .into_response()
562}
563
564async fn healthz() -> Response {
565    json_response(StatusCode::OK, "{\"status\":\"ok\"}")
566}
567
568/// 逐块收集 body 直到 cap 字节(Go 的 LimitReader + ReadAll 截断语义)。
569async fn collect_body(body: axum::body::Body, cap: usize) -> Vec<u8> {
570    use futures_util::StreamExt as _;
571    let mut out = Vec::new();
572    let mut stream = body.into_data_stream();
573    while let Some(Ok(chunk)) = stream.next().await {
574        let rem = cap.saturating_sub(out.len());
575        if rem == 0 {
576            break;
577        }
578        out.extend_from_slice(&chunk[..chunk.len().min(rem)]);
579    }
580    out
581}
582
583/// serve 模式:解析裸名 flag、装配路由与中间件、TLS/优雅关停。
584/// `:8080` 简写 → `0.0.0.0:8080`(Go net.Listen 惯例;tokio 的
585/// ToSocketAddrs 不接受略主机形态)。IPv6 缩写("::")原样保留。
586pub(crate) fn normalize_addr(addr: &str) -> String {
587    if let Some(rest) = addr.strip_prefix(':')
588        && !rest.contains(':')
589    {
590        return format!("0.0.0.0:{rest}");
591    }
592    addr.to_string()
593}
594
595pub(crate) fn serve(
596    ctx: &Ctx,
597    reg: &Registry,
598    args: &[String],
599    cfg: Config,
600    mount_mcp: bool,
601) -> i32 {
602    let cfg = crate::builtins::parse_serve_args(args, cfg);
603    let sctx = Arc::new(ctx.clone());
604    let mut router = match router_with(
605        reg,
606        Arc::clone(&sctx),
607        cfg.channel_defaults.clone(),
608        cfg.no_server_headers,
609        cfg.allow_unknown_query,
610        &cfg.resolved_name(),
611        &cfg.resolved_version(),
612    ) {
613        Ok(r) => r,
614        Err(e) => {
615            crate::logx::errorf(format_args!("{e}"));
616            return 2;
617        }
618    };
619    let mut mcp_note = String::new();
620    #[cfg(feature = "mcp")]
621    {
622        if mount_mcp
623            && let Some(m) = crate::mcp::mountable(
624                reg,
625                &crate::mcp::Options {
626                    // serve 挂载的 /mcp 与独立 mcp 模式共用同一应用身份
627                    // (spec §12.6 serverInfo):不继承 cfg 时挂载端回退到
628                    // bin_name/"0.0.0",与 stdio/http 模式报告不一致。
629                    name: cfg.resolved_name(),
630                    version: cfg.resolved_version(),
631                    bearer_tokens: cfg.bearer_tokens.clone(),
632                    default_ctx: Some(sctx.clone()),
633                    defaults: cfg.channel_defaults.clone(),
634                    ..Default::default()
635                },
636            )
637        {
638            router = router.nest_service("/mcp", m);
639            mcp_note = " + /mcp".to_string();
640        }
641    }
642    if !cfg.cors_origins.is_empty() {
643        crate::logx::debugf(format_args!(
644            "{}",
645            crate::lang::tf("log.cors_on", &[&format!("{:?}", cfg.cors_origins)])
646        ));
647    }
648    // 中间件链(由外到内):CORS 预检(鉴权前,浏览器预检不带凭据)→
649    // Bearer → Gzip → 路由。顺序语义在 middleware.rs 集中组装并有测试锁定。
650    let router = middleware::apply(
651        router,
652        cfg.bearer_tokens.clone(),
653        cfg.cors_origins.clone(),
654        cfg.timeout,
655    );
656    // §11.6 服务器上下文头:最外层(含 401/预检等一切响应)。
657    let router = router.layer(axum::middleware::from_fn(middleware::server_context_mw(
658        cfg.resolved_name(),
659        cfg.resolved_version(),
660        cfg.response_headers.clone(),
661        !cfg.no_server_headers,
662    )));
663
664    let tls_on = !cfg.cert_file.is_empty() || !cfg.key_file.is_empty();
665    if tls_on && (cfg.cert_file.is_empty() || cfg.key_file.is_empty()) {
666        crate::logx::errorf(format_args!("TLS requires both --tls-cert and --tls-key"));
667        return 2;
668    }
669    let scheme = if tls_on { "https" } else { "http" };
670    crate::logx::infof(format_args!(
671        "{}",
672        crate::lang::tf("log.serve_listening", &[scheme, &cfg.addr, &mcp_note])
673    ));
674
675    let rt = match tokio::runtime::Builder::new_multi_thread()
676        .enable_all()
677        .build()
678    {
679        Ok(rt) => rt,
680        Err(e) => {
681            crate::logx::errorf(format_args!("{e}"));
682            return 1;
683        }
684    };
685    rt.block_on(async move {
686        let listener =
687            match tokio::net::TcpListener::bind(&crate::httpapi::normalize_addr(&cfg.addr)).await {
688                Ok(l) => l,
689                Err(e) => {
690                    crate::logx::errorf(format_args!("{e}"));
691                    return 1;
692                }
693            };
694        let handle = axum_server::Handle::new();
695        let cert_file = cfg.cert_file.clone();
696        let key_file = cfg.key_file.clone();
697        let serve = {
698            let router = router.clone();
699            let handle = handle.clone();
700            move || async move {
701                if tls_on {
702                    let tls_config = match load_rustls_config(&cert_file, &key_file) {
703                        Ok(c) => c,
704                        Err(e) => {
705                            crate::logx::errorf(format_args!("{e}"));
706                            return 1;
707                        }
708                    };
709                    let rustls_cfg = axum_server::tls_rustls::RustlsConfig::from_config(
710                        std::sync::Arc::new(tls_config),
711                    );
712                    let server = match axum_server::from_tcp_rustls(
713                        listener.into_std().unwrap(),
714                        rustls_cfg,
715                    ) {
716                        Ok(s) => s,
717                        Err(e) => {
718                            crate::logx::errorf(format_args!("{e}"));
719                            return 1;
720                        }
721                    };
722                    let _ = server
723                        .handle(handle.clone())
724                        .serve(router.clone().into_make_service())
725                        .await;
726                } else {
727                    let server = match axum_server::from_tcp(listener.into_std().unwrap()) {
728                        Ok(s) => s,
729                        Err(e) => {
730                            crate::logx::errorf(format_args!("{e}"));
731                            return 1;
732                        }
733                    };
734                    let _ = server
735                        .handle(handle.clone())
736                        .serve(router.clone().into_make_service())
737                        .await;
738                };
739                0
740            }
741        };
742        let _ = &serve;
743        let server_handle = handle.clone();
744        let serve_fut = tokio::spawn(async move { serve().await });
745        let cancel_fut = async move {
746            sctx.cancelled_async().await;
747            server_handle.graceful_shutdown(Some(std::time::Duration::from_secs(5)));
748        };
749        tokio::select! {
750            join_res = serve_fut => join_res.unwrap_or(1),
751            _ = cancel_fut => {
752                crate::logx::infof(format_args!("{}", crate::lang::t("log.graceful")));
753                0
754            }
755        }
756    })
757}
758
759#[cfg(feature = "http-stack")]
760fn load_rustls_config(cert_file: &str, key_file: &str) -> errors::Result<rustls::ServerConfig> {
761    use std::io::BufReader;
762    let certs = rustls_pemfile::certs(&mut BufReader::new(
763        std::fs::File::open(cert_file).map_err(errors::Error::from)?,
764    ))
765    .collect::<Result<Vec<_>, _>>()
766    .map_err(|e| errors::Error::new(errors::Kind::Internal, format!("cert parse: {e}")))?;
767    let key = rustls_pemfile::private_key(&mut BufReader::new(
768        std::fs::File::open(key_file).map_err(errors::Error::from)?,
769    ))
770    .map_err(|e| errors::Error::new(errors::Kind::Internal, format!("key parse: {e}")))?
771    .ok_or_else(|| {
772        errors::Error::new(errors::Kind::Internal, "no private key found".to_string())
773    })?;
774    // 嵌入方的依赖图可能同时编入多个 crypto provider(rustls 自动选择会
775    // panic);显式安装 ring,重复安装(已装过)时忽略错误。
776    let _ = rustls::crypto::ring::default_provider().install_default();
777    let config = rustls::ServerConfig::builder()
778        .with_no_client_auth()
779        .with_single_cert(certs, key)
780        .map_err(|e| errors::Error::new(errors::Kind::Internal, format!("tls config: {e}")))?;
781    Ok(config)
782}