# Body 匹配优化:基于 Content-Type 的智能解析 ## Context **问题**: 当前只支持 JSON body 匹配,非 JSON 请求无法正确匹配。 **需求**: 根据请求的 Content-Type 智能解析 body,同时支持 header 匹配校验。 --- ## 核心设计 ### 关键原则 1. **Body 解析**:始终以【请求的 Content-Type】为准,因为这是数据的真实格式 2. **Header 匹配**:Content-Type 当作普通 header 处理,写了就匹配,没写跳过 3. **自动补充的 header**:Content-Length、Accept-Encoding 等不参与匹配(除非 YAML 显式配置) ### 处理流程 ``` ┌─────────────────────────────────────────────────────────────┐ │ 请求处理流程 │ ├─────────────────────────────────────────────────────────────┤ │ 1. 解析请求 body │ │ └── 始终以【请求的 Content-Type】为准解析 │ │ │ │ 2. Header 匹配(包含 Content-Type) │ │ └── YAML 写了就匹配,没写就跳过 │ │ │ │ 3. Body 匹配 │ │ └── 根据匹配成功的 Content-Type 决定比较方式 │ └─────────────────────────────────────────────────────────────┘ ``` ### 匹配示例 | YAML Content-Type | 请求 Content-Type | Header 匹配 | Body 解析 | 结果 | |-------------------|-------------------|-------------|-----------|------| | `application/xml` | `application/json` | 失败 | - | 不匹配 | | `application/json` | `application/json` | 成功 | JSON | 比较 body | | 无 | `application/json` | 跳过 | JSON | 比较 body | | 无 | `application/xml` | 跳过 | XML | 比较 body | | 无 | 无 | 跳过 | 字符串 | 比较 body | --- ## 实现方案 ### Step 1: 新增数据结构 (model.rs) ```rust /// 解析后的请求 Body #[derive(Debug, Clone)] pub enum ParsedBody { Json(serde_json::Value), Xml(String), Form(HashMap), Multipart(Vec), // 字段名列表 Text(String), None, } impl ParsedBody { /// 转换为字符串(用于兜底比较) pub fn to_compare_string(&self) -> String { match self { ParsedBody::Json(v) => v.to_string(), ParsedBody::Xml(s) | ParsedBody::Text(s) => s.clone(), ParsedBody::Form(map) => { let mut pairs: Vec<_> = map.iter().collect(); pairs.sort_by_key(|(k, _)| *k); pairs.iter().map(|(k, v)| format!("{}={}", k, v)).join("&") } ParsedBody::Multipart(fields) => fields.join(","), ParsedBody::None => String::new(), } } /// 获取对应的 Content-Type 名称 pub fn content_type_name(&self) -> &'static str { match self { ParsedBody::Json(_) => "application/json", ParsedBody::Xml(_) => "application/xml", ParsedBody::Form(_) => "application/x-www-form-urlencoded", ParsedBody::Multipart(_) => "multipart/form-data", ParsedBody::Text(_) => "text/plain", ParsedBody::None => "none", } } } ``` ### Step 2: Body 解析函数 (handler.rs) ```rust /// 提取请求的 Content-Type(去掉参数部分,如 boundary) fn extract_content_type(headers: &HeaderMap) -> Option { headers .get(axum::http::header::CONTENT_TYPE) .and_then(|v| v.to_str().ok()) .map(|s| s.split(';').next().unwrap_or(s).trim().to_lowercase()) } /// 根据 Content-Type 解析 Body(始终以请求的 Content-Type 为准) fn parse_body(content_type: Option<&str>, bytes: &[u8]) -> ParsedBody { if bytes.is_empty() { return ParsedBody::None; } match content_type { Some(ct) if ct.contains("application/json") => { serde_json::from_slice(bytes) .map(ParsedBody::Json) .unwrap_or_else(|_| { // JSON 解析失败,降级为文本 ParsedBody::Text(String::from_utf8_lossy(bytes).to_string()) }) } Some(ct) if ct.contains("xml") => { ParsedBody::Xml(String::from_utf8_lossy(bytes).to_string()) } Some(ct) if ct.contains("form-urlencoded") => { ParsedBody::Form(parse_urlencoded(bytes)) } Some(ct) if ct.contains("multipart/form-data") => { ParsedBody::Multipart(extract_multipart_fields(bytes)) } _ => { ParsedBody::Text(String::from_utf8_lossy(bytes).to_string()) } } } /// 解析 urlencoded 格式 fn parse_urlencoded(bytes: &[u8]) -> HashMap { let body = String::from_utf8_lossy(bytes); let mut map = HashMap::new(); for pair in body.split('&') { if let Some((key, value)) = pair.split_once('=') { // URL 解码 let decoded_key = urlencoding_decode(key); let decoded_value = urlencoding_decode(value); map.insert(decoded_key, decoded_value); } } map } ``` ### Step 3: 修改 handler.rs 主函数 ```rust pub async fn mock_handler( State(state): State>, method: Method, headers: HeaderMap, Query(params): Query>, req: Request, ) -> impl IntoResponse { let path = req.uri().path().to_string(); let method_str = method.as_str().to_string(); // 1. 提取请求的 Content-Type let req_content_type = extract_content_type(&headers); // 2. 读取请求 body let body_bytes = match axum::body::to_bytes(req.into_body(), 10 * 1024 * 1024).await { Ok(bytes) => bytes, Err(_) => { return Response::builder() .status(StatusCode::BAD_REQUEST) .body(Body::from("Read body error")) .unwrap(); } }; // 3. 根据【请求的 Content-Type】解析 body let parsed_body = parse_body(req_content_type.as_deref(), &body_bytes); // 4. 转换 headers 为 HashMap let mut req_headers = HashMap::new(); for (name, value) in headers.iter() { if let Ok(v) = value.to_str() { req_headers.insert(name.as_str().to_string(), v.to_string()); } } // 5. 执行匹配 let maybe_rule = { let router = state.router.read().expect("Failed to acquire read lock"); router.match_rule(&method_str, &path, ¶ms, &req_headers, &parsed_body).cloned() }; // 6. 构建响应(与现有逻辑相同) // ... } ``` ### Step 4: 修改 router.rs 匹配逻辑 ```rust /// 核心匹配函数 pub fn match_rule( &self, method: &str, path: &str, queries: &HashMap, headers: &HashMap, parsed_body: &ParsedBody, // 改为 ParsedBody ) -> Option<&MockRule> { let key = self.extract_first_segment(path); if let Some(rules) = self.index.get(&key) { for rule in rules { if self.is_match(rule, method, path, queries, headers, parsed_body) { return Some(rule); } } } None } fn is_match( &self, rule: &MockRule, method: &str, path: &str, queries: &HashMap, headers: &HashMap, parsed_body: &ParsedBody, ) -> bool { // A. Method 匹配 if rule.request.method.to_uppercase() != method.to_uppercase() { return false; } // B. Path 匹配 if rule.request.path.trim_end_matches('/') != path.trim_end_matches('/') { return false; } // C. Query 匹配(子集匹配) if let Some(ref required_queries) = rule.request.query_params { for (key, val) in required_queries { if queries.get(key) != Some(val) { return false; } } } // D. Header 匹配(包含 Content-Type) // YAML 写了就匹配,没写就跳过 if let Some(ref required_headers) = rule.request.headers { for (key, val) in required_headers { let matched = headers.iter().any(|(k, v)| { k.to_lowercase() == key.to_lowercase() && v == val }); if !matched { return false; // Header 不匹配,包括 Content-Type } } } // E. Body 匹配 // YAML 写了 body 才匹配,没写跳过 if let Some(ref yaml_body) = rule.request.body { return self.match_body(yaml_body, parsed_body); } true } /// Body 匹配逻辑 fn match_body(&self, yaml_body: &serde_json::Value, parsed_body: &ParsedBody) -> bool { match parsed_body { ParsedBody::Json(actual) => { // JSON 对象比较 yaml_body == actual } ParsedBody::Xml(actual) => { // XML 字符串比较 yaml_body.as_str() .map(|expected| expected.trim() == actual.trim()) .unwrap_or(false) } ParsedBody::Form(actual) => { // Form 键值对比较(子集匹配) compare_form_with_yaml(yaml_body, actual) } ParsedBody::Multipart(actual_fields) => { // Multipart 字段名比较 compare_multipart_with_yaml(yaml_body, actual_fields) } ParsedBody::Text(actual) => { // 字符串比较 yaml_body.as_str() .map(|expected| expected.trim() == actual.trim()) .unwrap_or_else(|| yaml_body.to_string().trim() == actual.trim()) } ParsedBody::None => { false // YAML 配置了 body,但请求没有 body } } } /// Form 比较:YAML 中的键值对必须是请求的子集 fn compare_form_with_yaml(yaml_body: &serde_json::Value, actual: &HashMap) -> bool { let yaml_map = match yaml_body.as_object() { Some(obj) => obj, None => return false, }; for (key, yaml_val) in yaml_map { let expected = yaml_val.as_str().unwrap_or(&yaml_val.to_string()); if actual.get(key) != Some(&expected.to_string()) { return false; } } true } ``` --- ## 需要修改的文件 | 文件 | 改动 | |------|------| | `src/model.rs` | 新增 `ParsedBody` 枚举和相关方法 | | `src/handler.rs` | 新增 `extract_content_type()`、`parse_body()`、修改 `mock_handler()` | | `src/router.rs` | 修改 `match_rule()` 和 `is_match()` 参数,新增 `match_body()` | | `Cargo.toml` | 可能需要 `urlencoding` 依赖 | --- ## YAML 配置示例 ```yaml # 严格匹配:要求 Content-Type + body - id: login-strict request: method: POST path: /api/login headers: Content-Type: application/json body: username: admin password: "123456" # 宽松匹配:只匹配 method + path + body(不检查 Content-Type) - id: login-loose request: method: POST path: /api/login body: username: admin # 最宽松:只匹配 method + path - id: any-body request: method: POST path: /api/echo ``` --- ## 验证 1. `cargo test` 2. 手动测试: - YAML 配置 `Content-Type: application/json`,请求发送 JSON → 匹配 - YAML 配置 `Content-Type: application/xml`,请求发送 JSON → Header 不匹配 - YAML 无 Content-Type,请求发送 XML → Body 字符串比较 - YAML 无 body 配置 → 跳过 body 检查