use std::collections::HashMap;
use axum::body::Body;
use axum::http::Request;
use http_body_util::BodyExt;
use serde_json::{Map, Value};
pub async fn fetch_post_data(req: Request<Body>) -> Result<Value, String> {
let query_map = parse_query(req.uri().query().unwrap_or(""));
let (parts, body) = req.into_parts();
let bytes = body
.collect()
.await
.map_err(|e| format!("read body failed: {e}"))?
.to_bytes();
let content_type = parts
.headers
.get(axum::http::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_lowercase();
let mut result = Map::new();
for (k, v) in query_map {
result.insert(k, Value::String(v));
}
if !bytes.is_empty() {
if content_type.contains("application/json") {
let body_value: Value =
serde_json::from_slice(&bytes).map_err(|e| format!("invalid JSON body: {e}"))?;
if let Value::Object(body_map) = body_value {
for (k, v) in body_map {
result.insert(k, v);
}
} else {
result.insert("data".to_string(), body_value);
}
} else if content_type.contains("application/x-www-form-urlencoded") {
let body_str = String::from_utf8_lossy(&bytes);
let body_map = parse_query(&body_str);
for (k, v) in body_map {
result.insert(k, Value::String(v));
}
} else {
if let Ok(body_value) = serde_json::from_slice::<Value>(&bytes) {
if let Value::Object(body_map) = body_value {
for (k, v) in body_map {
result.insert(k, v);
}
} else {
result.insert("data".to_string(), body_value);
}
} else {
let raw = String::from_utf8_lossy(&bytes).to_string();
if !raw.is_empty() {
result.insert("data".to_string(), Value::String(raw));
}
}
}
}
Ok(Value::Object(result))
}
pub async fn fetch_post_data_by_key(
req: Request<Body>,
key: &str,
) -> Result<Option<Value>, String> {
let data = fetch_post_data(req).await?;
Ok(data.get(key).cloned())
}
pub async fn fetch_body_data(req: Request<Body>) -> Result<Value, String> {
let (parts, body) = req.into_parts();
let bytes = body
.collect()
.await
.map_err(|e| format!("read body failed: {e}"))?
.to_bytes();
let content_type = parts
.headers
.get(axum::http::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_lowercase();
let mut result = Map::new();
if bytes.is_empty() {
return Ok(Value::Object(result));
}
if content_type.contains("application/json") {
let body_value: Value =
serde_json::from_slice(&bytes).map_err(|e| format!("invalid JSON body: {e}"))?;
if let Value::Object(body_map) = body_value {
for (k, v) in body_map {
result.insert(k, v);
}
} else {
result.insert("data".to_string(), body_value);
}
} else if content_type.contains("application/x-www-form-urlencoded") {
let body_str = String::from_utf8_lossy(&bytes);
let body_map = parse_query(&body_str);
for (k, v) in body_map {
result.insert(k, Value::String(v));
}
} else if let Ok(body_value) = serde_json::from_slice::<Value>(&bytes) {
if let Value::Object(body_map) = body_value {
for (k, v) in body_map {
result.insert(k, v);
}
} else {
result.insert("data".to_string(), body_value);
}
} else {
let raw = String::from_utf8_lossy(&bytes).to_string();
result.insert("data".to_string(), Value::String(raw));
}
Ok(Value::Object(result))
}
pub fn fetch_query_data(req: &Request<Body>) -> Value {
let query_map = parse_query(req.uri().query().unwrap_or(""));
let mut result = Map::new();
for (k, v) in query_map {
result.insert(k, Value::String(v));
}
Value::Object(result)
}
pub fn fetch_query_data_by_key(req: &Request<Body>, key: &str) -> Option<Value> {
let query_map = parse_query(req.uri().query().unwrap_or(""));
query_map.get(key).map(|v| Value::String(v.clone()))
}
pub fn parse_query(query: &str) -> HashMap<String, String> {
let mut result = HashMap::new();
if query.is_empty() {
return result;
}
for pair in query.split('&') {
if pair.is_empty() {
continue;
}
let mut split = pair.splitn(2, '=');
let key = url_decode(split.next().unwrap_or(""));
let value = url_decode(split.next().unwrap_or(""));
result.insert(key, value);
}
result
}
pub fn url_decode(s: &str) -> String {
let mut bytes: Vec<u8> = Vec::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
match c {
'+' => bytes.push(b' '),
'%' => {
let h1 = chars.next();
let h2 = chars.next();
if let (Some(a), Some(b)) = (h1, h2) {
if let Ok(byte) = u8::from_str_radix(&format!("{a}{b}"), 16) {
bytes.push(byte);
} else {
bytes.push(b'%');
push_char_utf8(&mut bytes, a);
push_char_utf8(&mut bytes, b);
}
} else {
bytes.push(b'%');
}
}
_ => push_char_utf8(&mut bytes, c),
}
}
String::from_utf8_lossy(&bytes).into_owned()
}
fn push_char_utf8(bytes: &mut Vec<u8>, c: char) {
let mut buf = [0u8; 4];
bytes.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::{Method, Request, StatusCode};
fn make_json_request(body: &str, query: Option<&str>) -> Request<Body> {
let uri = match query {
Some(q) => format!("/?{q}"),
None => "/".to_string(),
};
Request::builder()
.method(Method::POST)
.uri(&uri)
.header("content-type", "application/json")
.body(Body::from(body.to_string()))
.unwrap()
}
fn make_form_request(body: &str, query: Option<&str>) -> Request<Body> {
let uri = match query {
Some(q) => format!("/?{q}"),
None => "/".to_string(),
};
Request::builder()
.method(Method::POST)
.uri(&uri)
.header("content-type", "application/x-www-form-urlencoded")
.body(Body::from(body.to_string()))
.unwrap()
}
#[test]
fn test_parse_query_empty() {
let m = parse_query("");
assert!(m.is_empty());
}
#[test]
fn test_parse_query_single_pair() {
let m = parse_query("key=value");
assert_eq!(m.get("key"), Some(&"value".to_string()));
assert_eq!(m.len(), 1);
}
#[test]
fn test_parse_query_multiple_pairs() {
let m = parse_query("a=1&b=2&c=3");
assert_eq!(m.get("a"), Some(&"1".to_string()));
assert_eq!(m.get("b"), Some(&"2".to_string()));
assert_eq!(m.get("c"), Some(&"3".to_string()));
}
#[test]
fn test_parse_query_no_value() {
let m = parse_query("key");
assert_eq!(m.get("key"), Some(&"".to_string()));
}
#[test]
fn test_parse_query_url_encoded() {
let m = parse_query("name=hello%20world&email=a%40b.com");
assert_eq!(m.get("name"), Some(&"hello world".to_string()));
assert_eq!(m.get("email"), Some(&"a@b.com".to_string()));
}
#[test]
fn test_parse_query_plus_for_space() {
let m = parse_query("q=hello+world");
assert_eq!(m.get("q"), Some(&"hello world".to_string()));
}
#[test]
fn test_parse_query_skip_empty_pairs() {
let m = parse_query("a=1&&b=2&");
assert_eq!(m.len(), 2);
assert_eq!(m.get("a"), Some(&"1".to_string()));
assert_eq!(m.get("b"), Some(&"2".to_string()));
}
#[test]
fn test_url_decode_basic() {
assert_eq!(url_decode("hello"), "hello");
assert_eq!(url_decode("hello%20world"), "hello world");
assert_eq!(url_decode("a%40b"), "a@b");
assert_eq!(url_decode("a+b"), "a b");
}
#[test]
fn test_url_decode_utf8_multibyte() {
assert_eq!(url_decode("%E9%B2%9C%E8%A7%86%E8%BE%BE"), "鲜视达");
assert_eq!(url_decode("%E5%B7%A5%E5%85%B7%E7%AE%B1"), "工具箱");
assert_eq!(
url_decode("q=%E9%B2%9C%E8%A7%86%E8%BE%BE+plus"),
"q=鲜视达 plus"
);
}
#[test]
fn test_url_decode_invalid_utf8_lossy() {
let decoded = url_decode("%FF%FE");
assert!(decoded.contains('\u{FFFD}'));
}
#[test]
fn test_url_decode_trailing_percent() {
assert_eq!(url_decode("100%"), "100%");
}
#[tokio::test]
async fn test_fetch_post_data_json_body_only() {
let req = make_json_request(r#"{"name":"alice","age":30}"#, None);
let data = fetch_post_data(req).await.unwrap();
assert_eq!(data["name"], "alice");
assert_eq!(data["age"], 30);
}
#[tokio::test]
async fn test_fetch_post_data_query_only() {
let req = make_json_request("", Some("page=1&size=10"));
let data = fetch_post_data(req).await.unwrap();
assert_eq!(data["page"], "1");
assert_eq!(data["size"], "10");
}
#[tokio::test]
async fn test_fetch_post_data_body_overrides_query() {
let req = make_json_request(r#"{"page":99}"#, Some("page=1&size=10"));
let data = fetch_post_data(req).await.unwrap();
assert_eq!(data["page"], 99); assert_eq!(data["size"], "10"); }
#[tokio::test]
async fn test_fetch_post_data_form_urlencoded() {
let req = make_form_request("name=bob&age=25", None);
let data = fetch_post_data(req).await.unwrap();
assert_eq!(data["name"], "bob");
assert_eq!(data["age"], "25");
}
#[tokio::test]
async fn test_fetch_post_data_empty_body() {
let req = make_json_request("", None);
let data = fetch_post_data(req).await.unwrap();
assert!(data.as_object().unwrap().is_empty());
}
#[tokio::test]
async fn test_fetch_post_data_invalid_json() {
let req = make_json_request("{invalid}", None);
let result = fetch_post_data(req).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_fetch_post_data_by_key() {
let req = make_json_request(r#"{"name":"alice","age":30}"#, None);
let name = fetch_post_data_by_key(req, "name").await.unwrap();
assert_eq!(name, Some(Value::String("alice".to_string())));
}
#[tokio::test]
async fn test_fetch_post_data_by_key_missing() {
let req = make_json_request(r#"{"name":"alice"}"#, None);
let age = fetch_post_data_by_key(req, "age").await.unwrap();
assert_eq!(age, None);
}
#[tokio::test]
async fn test_fetch_post_data_array_value_in_body() {
let req = make_json_request(r#"{"ids":[1,2,3]}"#, None);
let data = fetch_post_data(req).await.unwrap();
assert_eq!(data["ids"], serde_json::json!([1, 2, 3]));
}
#[tokio::test]
async fn test_fetch_post_data_nested_object_in_body() {
let req = make_json_request(r#"{"user":{"name":"alice","age":30}}"#, None);
let data = fetch_post_data(req).await.unwrap();
assert_eq!(data["user"]["name"], "alice");
assert_eq!(data["user"]["age"], 30);
}
#[tokio::test]
async fn test_fetch_body_data_json() {
let req = make_json_request(r#"{"name":"alice"}"#, Some("ignored=1"));
let data = fetch_body_data(req).await.unwrap();
assert_eq!(data["name"], "alice");
assert!(data.get("ignored").is_none());
}
#[tokio::test]
async fn test_fetch_body_data_empty() {
let req = make_json_request("", None);
let data = fetch_body_data(req).await.unwrap();
assert!(data.as_object().unwrap().is_empty());
}
#[test]
fn test_fetch_query_data_basic() {
let req = Request::builder()
.method(Method::GET)
.uri("/?page=1&size=10")
.body(Body::empty())
.unwrap();
let data = fetch_query_data(&req);
assert_eq!(data["page"], "1");
assert_eq!(data["size"], "10");
}
#[test]
fn test_fetch_query_data_no_query() {
let req = Request::builder()
.method(Method::GET)
.uri("/")
.body(Body::empty())
.unwrap();
let data = fetch_query_data(&req);
assert!(data.as_object().unwrap().is_empty());
}
#[test]
fn test_fetch_query_data_by_key_found() {
let req = Request::builder()
.method(Method::GET)
.uri("/?page=1&size=10")
.body(Body::empty())
.unwrap();
assert_eq!(
fetch_query_data_by_key(&req, "page"),
Some(Value::String("1".to_string()))
);
assert_eq!(
fetch_query_data_by_key(&req, "size"),
Some(Value::String("10".to_string()))
);
}
#[test]
fn test_fetch_query_data_by_key_not_found() {
let req = Request::builder()
.method(Method::GET)
.uri("/?page=1")
.body(Body::empty())
.unwrap();
assert_eq!(fetch_query_data_by_key(&req, "missing"), None);
}
#[tokio::test]
async fn test_post_data_via_axum_handler() {
use axum::routing::post;
use tower::ServiceExt;
async fn handler(req: Request<Body>) -> (StatusCode, String) {
let data = fetch_post_data(req).await.unwrap();
let name = data["name"].as_str().unwrap_or("unknown");
let age = data["age"].as_i64().unwrap_or(0);
(StatusCode::OK, format!("{name} is {age}"))
}
let router = axum::Router::new().route("/", post(handler));
let req = Request::builder()
.method(Method::POST)
.uri("/")
.header("content-type", "application/json")
.body(Body::from(r#"{"name":"alice","age":30}"#))
.unwrap();
let resp = router.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
use http_body_util::BodyExt;
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
assert_eq!(&bytes[..], b"alice is 30");
}
#[tokio::test]
async fn test_php_consistency_post_data_merges_body_and_query() {
let req = make_json_request(r#"{"page":99}"#, Some("page=1&size=10"));
let data = fetch_post_data(req).await.unwrap();
assert_eq!(data["page"], 99, "body 应覆盖 query 同名字段");
assert_eq!(data["size"], "10", "query 字段应保留");
}
#[tokio::test]
async fn test_php_consistency_post_data_form_urlencoded_body() {
let req = make_form_request("name=bob&age=25", None);
let data = fetch_post_data(req).await.unwrap();
assert_eq!(data["name"], "bob");
assert_eq!(data["age"], "25");
}
#[tokio::test]
async fn test_php_consistency_post_data_by_key_returns_value() {
let req = make_json_request(r#"{"name":"alice","age":30}"#, None);
let name = fetch_post_data_by_key(req, "name").await.unwrap();
assert_eq!(name, Some(Value::String("alice".to_string())));
}
#[test]
fn test_php_consistency_get_data_returns_only_query() {
let req = Request::builder()
.method(Method::GET)
.uri("/?page=1&size=10")
.body(Body::empty())
.unwrap();
let data = fetch_query_data(&req);
assert_eq!(data["page"], "1");
assert_eq!(data["size"], "10");
assert!(data.get("body").is_none());
}
#[test]
fn test_php_consistency_get_data_by_key_returns_query_value() {
let req = Request::builder()
.method(Method::GET)
.uri("/?page=1&size=10")
.body(Body::empty())
.unwrap();
assert_eq!(
fetch_query_data_by_key(&req, "page"),
Some(Value::String("1".to_string()))
);
assert_eq!(fetch_query_data_by_key(&req, "missing"), None);
}
}