Files
mock-server/src/handler.rs
CNWei d364307131 feat: mock配置迁移至JSON格式并修复body匹配
- 将mock配置从YAML格式迁移到JSON格式
- 修复JSON字符串格式body匹配失败问题
- 添加MCP功能模块
- 更新mock-spec.md规范文档

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 09:43:11 +08:00

236 lines
7.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use axum::{
body::Body,
extract::{Query, State},
http::{HeaderMap, Method, Request, StatusCode},
response::{IntoResponse, Response},
};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use tokio_util::io::ReaderStream;
use crate::models::Payload;
use crate::router::MockRouter;
/// 共享的应用状态router 现在由 RwLock 保护以支持热重载
pub struct AppState {
pub router: RwLock<MockRouter>,
}
/// 提取请求的 Content-Type去掉参数部分如 boundary
fn extract_content_type(headers: &HeaderMap) -> Option<String> {
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]) -> Payload {
if bytes.is_empty() {
return Payload::None;
}
match content_type {
Some(ct) if ct.contains("application/json") => {
serde_json::from_slice(bytes)
.map(Payload::Json)
.unwrap_or_else(|_| {
// JSON 解析失败,降级为文本
Payload::Text(String::from_utf8_lossy(bytes).to_string())
})
}
Some(ct) if ct.contains("xml") => {
Payload::Xml(String::from_utf8_lossy(bytes).to_string())
}
Some(ct) if ct.contains("form-urlencoded") => {
Payload::Form(parse_urlencoded(bytes))
}
Some(ct) if ct.contains("multipart/form-data") => {
Payload::Multipart(extract_multipart_data(bytes))
}
_ => {
Payload::Text(String::from_utf8_lossy(bytes).to_string())
}
}
}
/// 解析 urlencoded 格式
fn parse_urlencoded(bytes: &[u8]) -> HashMap<String, String> {
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('=') {
let decoded_key = urlencoding_decode(key);
let decoded_value = urlencoding_decode(value);
map.insert(decoded_key, decoded_value);
}
}
map
}
/// URL 解码(简单实现)
fn urlencoding_decode(s: &str) -> String {
let mut result = String::new();
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '+' {
result.push(' ');
} else if c == '%' {
let hex: String = chars.by_ref().take(2).collect();
if let Ok(byte) = u8::from_str_radix(&hex, 16) {
result.push(byte as char);
} else {
result.push('%');
result.push_str(&hex);
}
} else {
result.push(c);
}
}
result
}
/// 从 multipart body 中提取键值对
fn extract_multipart_data(bytes: &[u8]) -> HashMap<String, String> {
let body = String::from_utf8_lossy(bytes);
let mut map = HashMap::new();
// 分割 boundary
let lines: Vec<&str> = body.lines().collect();
let mut current_name: Option<String> = None;
let mut current_value = String::new();
let mut in_value = false;
for line in &lines {
// 检测 Content-Disposition 行,提取 name
if line.contains("Content-Disposition") && line.contains("name=") {
// 保存上一个字段的值
if let Some(name) = current_name.take() {
map.insert(name, current_value.trim().to_string());
current_value.clear();
}
// 提取 name 属性
if let Some(start) = line.find("name=\"") {
let start = start + 6;
if let Some(end) = line[start..].find('"') {
current_name = Some(line[start..start + end].to_string());
in_value = false;
}
}
} else if line.starts_with("Content-Type") {
// 跳过 Content-Type 行
continue;
} else if line.is_empty() {
// 空行后面是值
in_value = true;
} else if in_value {
// 收集值内容
if !current_value.is_empty() {
current_value.push('\n');
}
current_value.push_str(line);
}
}
// 保存最后一个字段
if let Some(name) = current_name {
map.insert(name, current_value.trim().to_string());
}
map
}
/// 全局统一请求处理函数
pub async fn mock_handler(
State(state): State<Arc<AppState>>, // State 必须是第一个或靠前的参数
method: Method,
headers: HeaderMap,
Query(params): Query<HashMap<String, String>>,
req: Request<Body>, // Request<Body> 必须是最后一个参数
) -> impl IntoResponse {
// 1. 提取 path 和 method
let path = req.uri().path().to_string();
let method_str = method.as_str().to_string();
// 2. 提取请求的 Content-Type
let req_content_type = extract_content_type(&headers);
// 3. 读取请求 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();
}
};
// 4. 根据【请求的 Content-Type】解析 body
let parsed_body = parse_body(req_content_type.as_deref(), &body_bytes);
// 5. 将 Axum HeaderMap 转换为简单的 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());
}
}
// 6. 执行匹配逻辑:先获取读锁 (Read Lock)
let maybe_rule = {
let router = state.router.read().expect("Failed to acquire read lock");
router.match_rule(&method_str, &path, &params, &req_headers, &parsed_body).cloned()
// 此处使用 .cloned() 以便尽早释放读锁,避免阻塞热重载写锁
};
if let Some(rule) = maybe_rule {
// 7. 处理模拟延迟
if let Some(ref settings) = rule.settings {
if let Some(delay) = settings.delay_ms {
tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
}
}
// 8. 构建响应
let status = StatusCode::from_u16(rule.response.status).unwrap_or(StatusCode::OK);
let mut response_builder = Response::builder().status(status);
if let Some(ref h) = rule.response.headers {
for (k, v) in h {
response_builder = response_builder.header(k, v);
}
}
// 9. Smart Body 逻辑
if let Some(file_path) = rule.response.get_file_path() {
match tokio::fs::File::open(file_path).await {
Ok(file) => {
let stream = ReaderStream::new(file);
let body = Body::from_stream(stream);
response_builder.body(body).unwrap()
}
Err(_) => Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!(
"Mock Error: File not found at {}",
file_path
)))
.unwrap(),
}
} else {
// 内联模式:直接返回字符串内容
response_builder
.body(Body::from(rule.response.body.clone()))
.unwrap()
}
} else {
// 匹配失败返回 404
Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::from("No mock rule matched this request"))
.unwrap()
}
}